How to Build Multi-Agent Systems with OpenAI Codex SDK: Orchestrating Autonomous Coding Agents

Building Multi-Agent Systems with the OpenAI Codex SDK: A Complete Tutorial
Published: August 2, 2026 | ChatGPT AI Hub
” alt=”Building Multi-Agent Systems with the OpenAI Codex SDK” />
1. What Is the OpenAI Codex SDK and How Does It Differ from the ChatGPT API?
The OpenAI Codex SDK, released in its current form in late 2025 and significantly expanded through 2026, is a purpose-built developer toolkit for orchestrating software engineering agents. Unlike the ChatGPT API, which exposes a general-purpose conversational model endpoint, the Codex SDK provides a structured abstraction layer specifically designed for autonomous code generation, execution, testing, and multi-agent coordination within software development pipelines.
Understanding the distinction is critical before you invest architectural effort in either direction. The ChatGPT API (via openai.chat.completions.create()) is a stateless request-response interface. You send a message thread, receive a completion, and manage all state, context, and orchestration yourself. The Codex SDK, by contrast, ships with built-in agent lifecycle management, sandboxed code execution environments, tool-use primitives tailored to engineering tasks, and a first-class multi-agent coordination layer.
For a deeper look at the foundational ChatGPT API patterns, see our guide on ChatGPT API integration. Here we focus exclusively on what the Codex SDK adds on top of that foundation.
| Feature | ChatGPT API | Codex SDK |
|---|---|---|
| Primary use case | General-purpose conversation and text generation | Software engineering automation and code agents |
| State management | Manual (you manage thread history) | Built-in agent session and memory management |
| Code execution | Via Code Interpreter tool (limited) | Native sandboxed execution with full filesystem access |
| Multi-agent support | None (must build from scratch) | First-class: coordinator, pipeline, swarm primitives |
| Tool integration | Function calling (JSON schema) | Typed tool registry with versioning and permissions |
| Monitoring | Basic token usage in response headers | Structured execution traces, span telemetry, cost attribution |
| Authentication | API key | API key + org-scoped agent tokens + RBAC |
| Pricing model | Per token (input/output) | Per token + per agent-execution-minute + sandbox compute |
The Codex SDK wraps the underlying codex-2 model family (optimized for code understanding, generation, and reasoning about software systems) while also allowing you to route specific sub-tasks to gpt-4.5 or o3 models for non-code reasoning tasks. This hybrid routing capability is one of the most powerful features for multi-agent system design, and we will explore it in depth throughout this tutorial.
For background on what the Codex platform offers at a higher level, refer to our comprehensive OpenAI Codex guide before proceeding.
” alt=”Codex SDK architecture diagram showing agent layers” />
2. Setting Up the Codex SDK: Installation, Authentication, and Configuration
Installation
The Codex SDK requires Python 3.11 or later. It is distributed via PyPI and installs cleanly into a virtual environment. The package includes the core SDK, the CLI tool for local agent testing, and optional extras for Slack integration and observability.
# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install the core SDK
pip install openai-codex-sdk>=2.4.0
# Install with Slack integration support
pip install "openai-codex-sdk[slack]>=2.4.0"
# Install with full observability stack (OpenTelemetry exporters)
pip install "openai-codex-sdk[observability]>=2.4.0"
# Verify installation
codex --version
# Expected: openai-codex-sdk 2.4.x
Authentication
The Codex SDK uses a layered authentication model. At minimum, you need an OpenAI API key with Codex SDK access enabled on your organization account. For multi-agent deployments in team environments, you will also configure organization-scoped agent tokens that enforce role-based access control (RBAC) on which agents can access which tools and repositories.
import os
from openai_codex import CodexClient, AgentConfig
# Method 1: Environment variable (recommended for production)
# Set OPENAI_API_KEY and CODEX_ORG_TOKEN in your environment
client = CodexClient()
# Method 2: Explicit credentials (useful for testing multiple orgs)
client = CodexClient(
api_key=os.environ["OPENAI_API_KEY"],
org_token=os.environ["CODEX_ORG_TOKEN"],
organization_id="org-your-org-id-here"
)
# Method 3: Config file (~/.codex/config.yaml)
# Run `codex auth login` to generate this interactively
client = CodexClient.from_config(profile="production")
Configuration File Structure
For complex multi-agent deployments, a codex.yaml project configuration file provides centralized control over agent defaults, model routing, sandbox settings, and cost limits. Place this file in your project root.
# codex.yaml
version: "2"
project:
name: "engineering-automation"
environment: "production"
defaults:
model: "codex-2"
max_tokens: 8192
temperature: 0.2
sandbox:
enabled: true
timeout_seconds: 300
memory_mb: 2048
network_access: false # Disable by default for security
agents:
coordinator:
model: "o3" # Use reasoning model for orchestration decisions
max_tokens: 4096
code_writer:
model: "codex-2"
sandbox:
network_access: false
code_reviewer:
model: "codex-2"
tools:
- static_analysis
- security_scan
test_runner:
model: "codex-2"
sandbox:
timeout_seconds: 600 # Tests may run longer
cost_limits:
per_run_usd: 5.00
per_agent_usd: 1.50
daily_usd: 50.00
alert_threshold_percent: 80
telemetry:
enabled: true
exporter: "otlp"
endpoint: "http://localhost:4317"
Initializing the Client with Configuration
from openai_codex import CodexClient
from openai_codex.config import ProjectConfig
# Load from codex.yaml in current directory
config = ProjectConfig.from_file("codex.yaml")
client = CodexClient(config=config)
# Verify connectivity and permissions
health = client.health_check()
print(f"SDK Version: {health.sdk_version}")
print(f"Models Available: {health.available_models}")
print(f"Sandbox Status: {health.sandbox_status}")
print(f"Org Permissions: {health.permissions}")
For a complete walkthrough of setting up your local development environment for AI coding tools, see our guide on AI coding assistant setup.
3. Designing Agent Architectures: Coordinator, Pipeline, and Swarm Patterns
Before writing a single line of agent code, you must choose an architectural pattern that matches your task’s characteristics. The Codex SDK natively supports three primary multi-agent patterns, each with distinct tradeoffs in complexity, cost, latency, and fault tolerance.
The Coordinator Pattern
In the coordinator pattern, a single orchestrator agent receives a high-level task, decomposes it into sub-tasks, assigns each sub-task to a specialized worker agent, and synthesizes the results. The coordinator uses a reasoning-optimized model (such as o3) while workers use the code-optimized codex-2 model. This pattern is ideal when tasks have complex interdependencies that require dynamic planning.
Best for: Large feature implementation, architecture design with multiple components, complex debugging sessions where the root cause is unknown.
Tradeoff: The coordinator creates a single point of failure and adds latency because worker tasks cannot begin until the coordinator has finished its decomposition step.
The Pipeline Pattern
In the pipeline pattern, agents are arranged in a fixed sequence. The output of each agent becomes the input to the next. This is the software engineering equivalent of a Unix pipe. Each agent in the pipeline is specialized for one transformation: write code → review code → run tests → generate documentation. The pipeline pattern is deterministic, easy to monitor, and straightforward to debug because you can inspect the output at each stage.
Best for: Code review workflows, CI/CD automation, structured transformation tasks where the sequence of operations is known in advance.
Tradeoff: Pipelines are sequential by default, so total latency is the sum of all agent latencies. Parallel pipeline variants exist but require explicit fork/join logic.
The Swarm Pattern
In the swarm pattern, multiple homogeneous agents work on different parts of a problem simultaneously, without a central coordinator. A lightweight dispatcher distributes tasks from a shared queue, and a separate aggregator collects and reconciles results. Swarms excel at embarrassingly parallel problems: scanning a large codebase for security vulnerabilities, running tests across multiple modules simultaneously, or generating documentation for hundreds of functions in parallel.
Best for: Large-scale codebase analysis, bulk code generation, parallel test execution, batch processing of independent tasks.
Tradeoff: Result aggregation and conflict resolution become complex when agents produce overlapping or contradictory outputs. You must design explicit reconciliation logic.
Access 40,000+ AI Prompts for ChatGPT, Claude & Codex — Free!
Subscribe to get instant access to our complete Notion Prompt Library — the largest curated collection of prompts for ChatGPT, Claude, OpenAI Codex, and other leading AI models. Optimized for real-world workflows across coding, research, content creation, and business.
| Pattern | Coordination | Parallelism | Complexity | Best Use Case |
|---|---|---|---|---|
| Coordinator | Centralized | Medium (parallel workers) | Medium | Dynamic task decomposition |
| Pipeline | Sequential | Low (serial by default) | Low | Structured multi-stage workflows |
| Swarm | Decentralized | High (fully parallel) | High | Bulk parallel processing |
” alt=”Multi-agent architecture patterns: coordinator, pipeline, swarm” />
4. Implementing Single Agent Task Execution
Before composing multi-agent systems, you must understand how a single Codex agent executes a task. The fundamental unit is the Agent object, which encapsulates a model configuration, a set of tools, a sandbox environment, and an execution context.
Creating a Basic Code-Writing Agent
from openai_codex import CodexClient, Agent, AgentConfig
from openai_codex.tools import FilesystemTool, ShellTool, StaticAnalysisTool
from openai_codex.sandbox import SandboxConfig
client = CodexClient()
# Configure the sandbox environment
sandbox_config = SandboxConfig(
base_image="python:3.11-slim",
timeout_seconds=120,
memory_mb=1024,
network_access=False,
working_directory="/workspace"
)
# Define the agent
code_writer = Agent(
client=client,
name="code_writer",
model="codex-2",
system_prompt="""You are an expert Python developer. When given a task:
1. Write clean, well-documented Python code
2. Follow PEP 8 style guidelines
3. Include type hints for all function signatures
4. Write docstrings for all public functions and classes
5. Handle edge cases and raise appropriate exceptions
Always output complete, runnable code files.""",
tools=[
FilesystemTool(permissions=["read", "write"], allowed_paths=["/workspace"]),
ShellTool(allowed_commands=["python", "pip", "pytest"])
],
sandbox=sandbox_config,
max_tokens=4096,
temperature=0.1 # Low temperature for deterministic code generation
)
# Execute a task
task_result = code_writer.execute(
task="""Create a Python module called `data_validator.py` that:
- Validates email addresses using regex
- Validates phone numbers (US format)
- Validates dates in ISO 8601 format
- Returns detailed error messages for invalid inputs
- Includes a ValidationResult dataclass with fields: is_valid, value, errors
""",
working_directory="/workspace/src"
)
print(f"Status: {task_result.status}") # SUCCESS, FAILED, TIMEOUT
print(f"Files Created: {task_result.files_created}")
print(f"Execution Time: {task_result.execution_time_ms}ms")
print(f"Tokens Used: {task_result.usage.total_tokens}")
print(f"Cost: ${task_result.cost_usd:.4f}")
# Access the generated code
for file_path, content in task_result.file_contents.items():
print(f"\n--- {file_path} ---")
print(content)
Executing Code and Capturing Test Results
from openai_codex import Agent
from openai_codex.tools import FilesystemTool, ShellTool, PytestTool
test_runner = Agent(
client=client,
name="test_runner",
model="codex-2",
system_prompt="""You are a test execution agent. Run pytest on the provided
test files, capture all output, and return a structured report including:
- Pass/fail counts
- Failed test names and error messages
- Code coverage percentage
- Performance metrics for slow tests (>1s)""",
tools=[
FilesystemTool(permissions=["read"], allowed_paths=["/workspace"]),
PytestTool(coverage=True, timeout_per_test=30)
],
sandbox=sandbox_config
)
test_result = test_runner.execute(
task="Run all tests in /workspace/tests/ and provide a detailed report",
context={
"source_directory": "/workspace/src",
"test_directory": "/workspace/tests"
}
)
# Access structured test output
report = test_result.structured_output
print(f"Tests Passed: {report['passed']}")
print(f"Tests Failed: {report['failed']}")
print(f"Coverage: {report['coverage_percent']}%")
if report['failed_tests']:
for test in report['failed_tests']:
print(f" FAIL: {test['name']} - {test['error']}")
The task_result.structured_output field is populated when the agent’s system prompt instructs it to produce JSON-structured output. The Codex SDK automatically parses and validates this output against an optional Pydantic schema you can provide via the output_schema parameter on the Agent constructor.
5. Task Delegation Between Agents
Task delegation is the mechanism by which one agent assigns work to another. In the Codex SDK, delegation is explicit and typed. The delegating agent calls a delegate() method on an AgentRegistry object, specifying the target agent by name, the task description, and any context data to pass along. The SDK handles serialization, sandboxing, and result propagation automatically.
Setting Up an Agent Registry
from openai_codex import CodexClient, Agent, AgentRegistry
from openai_codex.tools import FilesystemTool, ShellTool, GitTool, StaticAnalysisTool
client = CodexClient()
# Create specialized agents
agents = {
"code_writer": Agent(
client=client,
name="code_writer",
model="codex-2",
system_prompt="You write production-quality Python code based on specifications.",
tools=[FilesystemTool(permissions=["read", "write"]), ShellTool()],
temperature=0.1
),
"code_reviewer": Agent(
client=client,
name="code_reviewer",
model="codex-2",
system_prompt="""You review Python code for:
- Logic errors and bugs
- Security vulnerabilities (injection, XSS, insecure deserialization)
- Performance issues (N+1 queries, unnecessary loops, memory leaks)
- Style violations (PEP 8, naming conventions)
Return a structured review with severity levels: CRITICAL, HIGH, MEDIUM, LOW.""",
tools=[FilesystemTool(permissions=["read"]), StaticAnalysisTool()],
temperature=0.0
),
"test_writer": Agent(
client=client,
name="test_writer",
model="codex-2",
system_prompt="You write comprehensive pytest test suites for Python code.",
tools=[FilesystemTool(permissions=["read", "write"])],
temperature=0.1
),
"doc_writer": Agent(
client=client,
name="doc_writer",
model="gpt-4.5", # Use GPT-4.5 for prose quality
system_prompt="You write clear, accurate technical documentation in Markdown.",
tools=[FilesystemTool(permissions=["read", "write"])],
temperature=0.3
)
}
# Register all agents
registry = AgentRegistry(client=client)
for name, agent in agents.items():
registry.register(agent)
print(f"Registered agents: {registry.list_agents()}")
Implementing a Coordinator with Delegation
from openai_codex import Agent, AgentRegistry
from openai_codex.delegation import DelegationContext, DelegationResult
from openai_codex.tools import DelegationTool
import json
def create_coordinator(registry: AgentRegistry) -> Agent:
"""Create a coordinator agent that can delegate to registered workers."""
coordinator = Agent(
client=client,
name="coordinator",
model="o3", # Reasoning model for planning
system_prompt=f"""You are a software engineering coordinator.
You decompose complex engineering tasks and delegate to specialized agents.
Available agents: {json.dumps(registry.get_agent_capabilities())}
For each task:
1. Analyze the requirements
2. Create a delegation plan specifying which agent handles which sub-task
3. Specify dependencies between sub-tasks (which must complete before others start)
4. Define the expected output format for each delegation
5. Synthesize results into a coherent final output
Always return your plan as structured JSON before executing delegations.""",
tools=[
DelegationTool(registry=registry),
FilesystemTool(permissions=["read", "write"])
],
temperature=0.0,
max_tokens=8192
)
return coordinator
coordinator = create_coordinator(registry)
# Execute a complex task through the coordinator
result = coordinator.execute(
task="""Implement a REST API endpoint for user authentication:
- POST /auth/login accepting {email, password}
- JWT token generation with 24h expiry
- Bcrypt password hashing
- Rate limiting (5 attempts per minute per IP)
- Comprehensive tests
- API documentation
Use FastAPI. Follow security best practices."""
)
# The coordinator will automatically delegate to code_writer, code_reviewer,
# test_writer, and doc_writer based on its planning output
print(f"Coordinator Status: {result.status}")
print(f"Delegations Made: {len(result.delegation_log)}")
for delegation in result.delegation_log:
print(f" → {delegation.agent_name}: {delegation.status} "
f"({delegation.execution_time_ms}ms, ${delegation.cost_usd:.4f})")
The DelegationTool is the key primitive here. When the coordinator agent calls this tool, the SDK intercepts the call, routes it to the target agent in the registry, executes the sub-task in a separate sandbox, and returns the result to the coordinator’s context. The coordinator never directly executes code itself — it only reasons about task decomposition and result synthesis.
For advanced prompt engineering strategies to improve coordinator decision quality, see our guide on prompt engineering for developers.
6. Handling Inter-Agent Communication
Inter-agent communication in the Codex SDK goes beyond simple task delegation. Agents may need to share partial results, request clarification from other agents, broadcast state changes, or subscribe to events from a shared message bus. The SDK provides three communication primitives: direct delegation (synchronous), message passing (asynchronous), and shared state via the agent context store.
Asynchronous Message Passing
import asyncio
from openai_codex.async_client import AsyncCodexClient
from openai_codex.messaging import AgentMessageBus, Message, MessageType
async def main():
client = AsyncCodexClient()
bus = AgentMessageBus(client=client)
# Define message handlers
async def handle_review_request(message: Message):
"""Code reviewer handles incoming review requests."""
code_content = message.payload["code"]
file_path = message.payload["file_path"]
review_result = await code_reviewer.execute_async(
task=f"Review this code for issues:\n\n{code_content}",
context={"file_path": file_path}
)
# Publish review results back to the bus
await bus.publish(Message(
type=MessageType.RESULT,
sender="code_reviewer",
recipient=message.sender,
correlation_id=message.id,
payload={
"review": review_result.structured_output,
"file_path": file_path
}
))
async def handle_fix_request(message: Message):
"""Bug fixer handles incoming fix requests from reviewer."""
issues = message.payload["review"]["issues"]
file_path = message.payload["file_path"]
# Only process CRITICAL and HIGH severity issues
critical_issues = [i for i in issues if i["severity"] in ["CRITICAL", "HIGH"]]
if critical_issues:
fix_result = await code_writer.execute_async(
task=f"Fix these critical issues in {file_path}:\n{json.dumps(critical_issues, indent=2)}"
)
await bus.publish(Message(
type=MessageType.RESULT,
sender="bug_fixer",
recipient="coordinator",
correlation_id=message.correlation_id,
payload={"fixes_applied": fix_result.structured_output}
))
# Subscribe agents to message types
bus.subscribe("code_reviewer", MessageType.REVIEW_REQUEST, handle_review_request)
bus.subscribe("bug_fixer", MessageType.FIX_REQUEST, handle_fix_request)
# Start the message bus
await bus.start()
# Publish an initial task to kick off the workflow
await bus.publish(Message(
type=MessageType.REVIEW_REQUEST,
sender="coordinator",
recipient="code_reviewer",
payload={
"code": open("/workspace/src/auth.py").read(),
"file_path": "/workspace/src/auth.py"
}
))
# Wait for all messages to be processed
await bus.drain(timeout_seconds=300)
await bus.stop()
asyncio.run(main())
Shared State via Agent Context Store
from openai_codex.state import AgentContextStore, StateScope
# Initialize a shared context store
# StateScope.SESSION: persists for the duration of the current run
# StateScope.PROJECT: persists across runs (stored in Codex cloud)
# StateScope.EPHEMERAL: in-memory only, lost when process exits
store = AgentContextStore(
client=client,
scope=StateScope.SESSION,
namespace="code_review_pipeline"
)
# Writer agent stores its output
async def code_writer_task(store: AgentContextStore, spec: str):
result = await code_writer.execute_async(task=spec)
# Store result for other agents to access
await store.set("generated_code", {
"files": result.file_contents,
"summary": result.summary,
"timestamp": result.completed_at.isoformat()
})
await store.set("pipeline_stage", "code_written")
return result
# Reviewer agent reads writer's output
async def code_reviewer_task(store: AgentContextStore):
# Wait for code to be available
await store.wait_for_key("generated_code", timeout_seconds=60)
generated = await store.get("generated_code")
stage = await store.get("pipeline_stage")
if stage != "code_written":
raise RuntimeError(f"Unexpected pipeline stage: {stage}")
# Review all generated files
review_tasks = []
for file_path, content in generated["files"].items():
task = code_reviewer.execute_async(
task=f"Review {file_path}:\n\n{content}"
)
review_tasks.append(task)
reviews = await asyncio.gather(*review_tasks)
# Store aggregated reviews
await store.set("review_results", {
file_path: review.structured_output
for file_path, review in zip(generated["files"].keys(), reviews)
})
await store.set("pipeline_stage", "review_complete")
return reviews
7. Multi-Agent Pipeline for Code Review
A code review pipeline is one of the most practical applications of multi-agent systems in software engineering. The following implementation creates a production-grade pipeline that takes a pull request, runs it through a series of specialized review agents, and produces a consolidated review report with actionable feedback.
Pipeline Definition
from openai_codex import CodexClient
from openai_codex.pipeline import Pipeline, PipelineStage, PipelineConfig
from openai_codex.tools import (
GitTool, FilesystemTool, StaticAnalysisTool,
SecurityScanTool, DependencyCheckTool, PytestTool
)
from openai_codex.models import PipelineResult
from dataclasses import dataclass
from typing import List, Dict, Any
import asyncio
client = CodexClient()
@dataclass
class PRContext:
"""Context passed through the review pipeline."""
pr_number: int
repo_url: str
base_branch: str
head_branch: str
changed_files: List[str]
diff_content: str
pr_description: str
def build_code_review_pipeline() -> Pipeline:
"""Construct the complete code review pipeline."""
# Stage 1: Static Analysis Agent
static_analysis_stage = PipelineStage(
name="static_analysis",
agent=Agent(
client=client,
name="static_analyzer",
model="codex-2",
system_prompt="""Perform static analysis on the provided code diff.
Identify:
1. Syntax errors and type errors
2. Undefined variables and imports
3. Unreachable code
4. Complexity metrics (cyclomatic complexity > 10 is HIGH)
5. Code duplication (>20 lines duplicated is a flag)
Return JSON: {"issues": [{"file": str, "line": int, "type": str,
"severity": str, "message": str, "suggestion": str}],
"metrics": {"avg_complexity": float, "max_complexity": float}}""",
tools=[StaticAnalysisTool(linters=["pylint", "mypy", "radon"])],
temperature=0.0
),
input_transformer=lambda ctx: {
"task": f"Analyze this diff:\n\n{ctx.diff_content}",
"context": {"files": ctx.changed_files}
},
output_key="static_analysis"
)
# Stage 2: Security Review Agent
security_review_stage = PipelineStage(
name="security_review",
agent=Agent(
client=client,
name="security_reviewer",
model="codex-2",
system_prompt="""You are a security engineer reviewing code for vulnerabilities.
Check for:
1. SQL injection vulnerabilities
2. Command injection risks
3. Hardcoded credentials or API keys
4. Insecure deserialization
5. Missing input validation
6. OWASP Top 10 violations
7. Dependency vulnerabilities (check against CVE database)
Severity: CRITICAL (exploitable remotely), HIGH (exploitable with access),
MEDIUM (requires specific conditions), LOW (best practice violation)
Return JSON: {"vulnerabilities": [...], "risk_score": int (0-100),
"requires_security_review": bool}""",
tools=[
SecurityScanTool(scanners=["bandit", "semgrep"]),
DependencyCheckTool()
],
temperature=0.0
),
input_transformer=lambda ctx: {
"task": f"Security review for PR #{ctx.pr_number}:\n\n{ctx.diff_content}",
"context": {"pr_description": ctx.pr_description}
},
output_key="security_review"
)
# Stage 3: Logic Review Agent
logic_review_stage = PipelineStage(
name="logic_review",
agent=Agent(
client=client,
name="logic_reviewer",
model="codex-2",
system_prompt="""Review code changes for logical correctness.
Evaluate:
1. Algorithm correctness (does it do what the PR description claims?)
2. Edge case handling (null inputs, empty collections, overflow)
3. Race conditions and thread safety issues
4. Error handling completeness (are all exceptions caught appropriately?)
5. Business logic alignment with the PR description
6. API contract correctness (request/response shapes)
Return JSON: {"logic_issues": [...], "missing_edge_cases": [...],
"alignment_score": int (0-10), "overall_assessment": str}""",
tools=[FilesystemTool(permissions=["read"])],
temperature=0.1
),
input_transformer=lambda ctx, prev_results: {
"task": f"""Review logic in PR #{ctx.pr_number}:
Description: {ctx.pr_description}
Diff:
{ctx.diff_content}
Static analysis already found: {len(prev_results.get('static_analysis', {}).get('issues', []))} issues""",
},
output_key="logic_review",
depends_on=["static_analysis"] # Runs after static analysis
)
# Stage 4: Test Coverage Agent
test_coverage_stage = PipelineStage(
name="test_coverage",
agent=Agent(
client=client,
name="test_coverage_checker",
model="codex-2",
system_prompt="""Analyze test coverage for code changes.
Determine:
1. Which changed functions/classes have corresponding tests
2. Which changed functions/classes are NOT tested
3. Whether existing tests cover the new code paths
4. Suggest specific test cases that should be added
Return JSON: {"coverage_gaps": [...], "suggested_tests": [...],
"estimated_coverage_percent": float, "test_quality_score": int (0-10)}""",
tools=[
FilesystemTool(permissions=["read"]),
PytestTool(coverage=True, dry_run=True)
],
temperature=0.1
),
input_transformer=lambda ctx: {
"task": f"Check test coverage for changes in PR #{ctx.pr_number}:\n{ctx.diff_content}"
},
output_key="test_coverage"
)
# Stage 5: Report Synthesis Agent
synthesis_stage = PipelineStage(
name="synthesis",
agent=Agent(
client=client,
name="review_synthesizer",
model="gpt-4.5", # Better prose for human-readable output
system_prompt="""You synthesize multiple code review reports into a single,
actionable pull request review.
Format the output as a GitHub PR review comment with:
1. Executive summary (2-3 sentences)
2. MUST FIX items (blocking the merge)
3. SHOULD FIX items (important but non-blocking)
4. SUGGESTIONS (optional improvements)
5. POSITIVE FEEDBACK (what was done well)
6. Overall verdict: APPROVE / REQUEST_CHANGES / COMMENT
Be specific: include file names, line numbers, and concrete fix suggestions.""",
tools=[],
temperature=0.3
),
input_transformer=lambda ctx, prev_results: {
"task": f"""Synthesize this review for PR #{ctx.pr_number}: "{ctx.pr_description}"
Static Analysis: {prev_results['static_analysis']}
Security Review: {prev_results['security_review']}
Logic Review: {prev_results['logic_review']}
Test Coverage: {prev_results['test_coverage']}"""
},
output_key="final_review",
depends_on=["static_analysis", "security_review", "logic_review", "test_coverage"]
)
# Assemble the pipeline
pipeline = Pipeline(
client=client,
name="code_review_pipeline",
stages=[
static_analysis_stage,
security_review_stage,
logic_review_stage,
test_coverage_stage,
synthesis_stage
],
config=PipelineConfig(
parallel_stages=["static_analysis", "security_review", "test_coverage"],
max_parallel_agents=3,
fail_fast=True, # Stop pipeline if CRITICAL security issue found
cost_limit_usd=3.00
)
)
return pipeline
# Execute the pipeline
async def run_code_review(pr_context: PRContext):
pipeline = build_code_review_pipeline()
result: PipelineResult = await pipeline.execute_async(input_data=pr_context)
print(f"Pipeline Status: {result.status}")
print(f"Total Duration: {result.total_duration_ms}ms")
print(f"Total Cost: ${result.total_cost_usd:.4f}")
print(f"\n{'='*60}")
print("FINAL REVIEW:")
print(result.outputs["final_review"]["text"])
# Post review to GitHub if verdict is not APPROVE
verdict = result.outputs["final_review"]["verdict"]
if verdict != "APPROVE":
print(f"\nVerdict: {verdict} - Posting to GitHub PR #{pr_context.pr_number}")
return result
8. Autonomous Bug-Fixing Agent Swarm
The swarm pattern shines when you need to process many independent tasks in parallel. An autonomous bug-fixing swarm takes a list of known issues (from a bug tracker, test failures, or static analysis output) and dispatches multiple agents to fix them simultaneously. The challenge lies in preventing agents from making conflicting changes to shared files and in aggregating their fixes into a coherent, mergeable patch.
Swarm Infrastructure
from openai_codex import CodexClient, Agent
from openai_codex.swarm import AgentSwarm, SwarmConfig, TaskQueue, WorkerPool
from openai_codex.tools import FilesystemTool, GitTool, ShellTool
from openai_codex.locking import FileLockManager
from dataclasses import dataclass
from typing import List, Optional
import asyncio
import uuid
@dataclass
class BugTask:
"""Represents a single bug to be fixed by a swarm agent."""
id: str
file_path: str
line_number: int
issue_type: str
severity: str
description: str
suggested_fix: Optional[str] = None
test_command: Optional[str] = None
async def create_bug_fixing_swarm(
bug_tasks: List[BugTask],
repo_path: str,
max_workers: int = 5
) -> AgentSwarm:
"""Create and configure a bug-fixing agent swarm."""
client = CodexClient()
# File lock manager prevents multiple agents from editing the same file
lock_manager = FileLockManager(
lock_timeout_seconds=120,
deadlock_detection=True
)
# Worker agent factory - creates identical worker agents
def create_worker_agent(worker_id: int) -> Agent:
return Agent(
client=client,
name=f"bug_fixer_{worker_id}",
model="codex-2",
system_prompt=f"""You are bug-fixing agent #{worker_id}.
You fix one specific bug at a time. For each bug:
1. Read the affected file(s) to understand context
2. Implement the minimal fix that resolves the issue
3. Do NOT refactor unrelated code
4. Do NOT change function signatures unless required by the bug
5. Add a comment above your fix: # BUG FIX [{worker_id}]: <brief description>
6. Run the test command if provided to verify your fix
7. If the fix causes test failures, revert and report inability to fix
Return JSON: {{"fixed": bool, "files_modified": [str],
"fix_description": str, "tests_passed": bool, "error": str | null}}""",
tools=[
FilesystemTool(
permissions=["read", "write"],
allowed_paths=[repo_path],
lock_manager=lock_manager # Prevent concurrent file access
),
ShellTool(allowed_commands=["python", "pytest", "git"]),
GitTool(permissions=["diff", "add"])
],
temperature=0.05,
max_tokens=4096
)
# Create worker pool
worker_pool = WorkerPool(
workers=[create_worker_agent(i) for i in range(max_workers)],
max_concurrent_tasks=max_workers
)
# Create task queue from bug list
task_queue = TaskQueue()
for bug in bug_tasks:
# Group bugs by file to prevent conflicts
task_queue.enqueue(
task_id=bug.id,
task_data={
"task": f"""Fix this bug:
File: {bug.file_path}
Line: {bug.line_number}
Type: {bug.issue_type}
Severity: {bug.severity}
Description: {bug.description}
Suggested Fix: {bug.suggested_fix or 'Determine appropriate fix'}
Test Command: {bug.test_command or 'Run relevant tests'}""",
"file_path": bug.file_path,
"priority": 1 if bug.severity == "CRITICAL" else 2
},
priority=1 if bug.severity == "CRITICAL" else 2
)
swarm = AgentSwarm(
client=client,
worker_pool=worker_pool,
task_queue=task_queue,
config=SwarmConfig(
max_retries_per_task=2,
retry_on_failure=True,
file_conflict_strategy="queue", # Queue conflicting tasks, don't skip
cost_limit_usd=10.00,
progress_callback=lambda progress: print(
f"Progress: {progress.completed}/{progress.total} "
f"({progress.success_rate:.1%} success rate)"
)
)
)
return swarm
# Execute the swarm
async def run_bug_fixing_swarm(bugs: List[BugTask], repo_path: str):
swarm = await create_bug_fixing_swarm(bugs, repo_path, max_workers=5)
print(f"Starting swarm with {len(bugs)} bugs to fix...")
swarm_result = await swarm.execute_async()
print(f"\nSwarm Complete:")
print(f" Total Bugs: {swarm_result.total_tasks}")
print(f" Fixed: {swarm_result.successful_tasks}")
print(f" Failed: {swarm_result.failed_tasks}")
print(f" Skipped (conflicts): {swarm_result.skipped_tasks}")
print(f" Total Cost: ${swarm_result.total_cost_usd:.4f}")
print(f" Duration: {swarm_result.total_duration_ms / 1000:.1f}s")
# Show failures for investigation
for failed in swarm_result.failed_task_results:
print(f"\n FAILED: {failed.task_id}")
print(f" Error: {failed.error}")
print(f" Agent: {failed.agent_name}")
return swarm_result
Conflict Detection and Resolution in the Swarm
from openai_codex.swarm import ConflictDetector, ConflictResolutionStrategy
from openai_codex.diff import DiffMerger, MergeConflict
async def resolve_swarm_conflicts(swarm_result) -> dict:
"""Merge all agent fixes and resolve any conflicts."""
conflict_detector = ConflictDetector()
diff_merger = DiffMerger(strategy=ConflictResolutionStrategy.SEMANTIC)
# Collect all diffs produced by swarm agents
all_diffs = {
task_id: result.git_diff
for task_id, result in swarm_result.successful_task_results.items()
if result.git_diff
}
# Detect conflicts between diffs
conflicts = conflict_detector.detect(all_diffs)
if not conflicts:
print("No conflicts detected. All fixes can be applied cleanly.")
merged_diff = diff_merger.merge_all(all_diffs)
return {"status": "clean", "merged_diff": merged_diff}
print(f"Detected {len(conflicts)} conflicts:")
resolved_diffs = dict(all_diffs)
conflict_resolutions = []
for conflict in conflicts:
print(f" Conflict: {conflict.file_path} "
f"(tasks: {conflict.task_id_a} vs {conflict.task_id_b})")
# Use a dedicated resolution agent for each conflict
resolution_agent = Agent(
client=client,
name="conflict_resolver",
model="o3", # Reasoning model for conflict resolution
system_prompt="""You resolve merge conflicts between two code fixes.
Given two overlapping diffs for the same file, produce a single unified diff
that correctly applies both fixes without breaking either.
Preserve the intent of both fixes.
Return the merged diff in unified diff format.""",
tools=[],
temperature=0.0
)
resolution = await resolution_agent.execute_async(
task=f"""Resolve conflict in {conflict.file_path}:
FIX A (Task {conflict.task_id_a}):
{conflict.diff_a}
FIX B (Task {conflict.task_id_b}):
{conflict.diff_b}
Original context:
{conflict.original_context}"""
)
resolved_diffs[f"resolved_{conflict.file_path}"] = resolution.text
conflict_resolutions.append({
"file": conflict.file_path,
"resolution": resolution.text
})
final_merged = diff_merger.merge_all(resolved_diffs)
return {
"status": "resolved",
"conflicts_found": len(conflicts),
"conflicts_resolved": len(conflict_resolutions),
"merged_diff": final_merged,
"resolutions": conflict_resolutions
}
9. Result Aggregation and Conflict Resolution
When multiple agents produce outputs that must be combined into a single coherent result, you need a principled aggregation strategy. The Codex SDK provides built-in aggregators for common patterns, and you can implement custom aggregation logic for domain-specific needs.
Voting Aggregator for Consensus Decisions
from openai_codex.aggregation import VotingAggregator, WeightedAggregator, AggregationResult
from openai_codex import Agent
from typing import List, Dict, Any
async def multi_agent_security_audit(code_file: str) -> AggregationResult:
"""Run multiple security agents and aggregate their findings by consensus."""
# Three independent security agents with different specializations
security_agents = [
Agent(client=client, name="owasp_agent", model="codex-2",
system_prompt="You specialize in OWASP Top 10 vulnerabilities."),
Agent(client=client, name="crypto_agent", model="codex-2",
system_prompt="You specialize in cryptographic vulnerabilities and key management."),
Agent(client=client, name="injection_agent", model="codex-2",
system_prompt="You specialize in injection attacks: SQL, command, LDAP, XPath.")
]
code_content = open(code_file).read()
# Run all agents in parallel
audit_tasks = [
agent.execute_async(
task=f"Audit this code for security vulnerabilities:\n\n{code_content}",
output_schema={
"type": "object",
"properties": {
"vulnerabilities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"type": {"type": "string"},
"severity": {"type": "string"},
"line": {"type": "integer"},
"description": {"type": "string"},
"cve": {"type": "string"}
}
}
}
}
}
)
for agent in security_agents
]
results = await asyncio.gather(*audit_tasks)
# Aggregate using voting: only include vulnerabilities found by 2+ agents
aggregator = VotingAggregator(
min_votes=2, # Require consensus from at least 2 of 3 agents
deduplication_key="type", # Deduplicate by vulnerability type
merge_strategy="highest_severity" # When merging, keep highest severity
)
aggregated = aggregator.aggregate(
results=[r.structured_output for r in results],
agent_names=[a.name for a in security_agents]
)
print(f"Individual findings: {aggregated.total_individual_findings}")
print(f"Consensus findings (2+ agents): {aggregated.consensus_count}")
print(f"Unique findings (1 agent only): {aggregated.unique_count}")
print(f"Confidence score: {aggregated.confidence:.2%}")
return aggregated
# Weighted aggregation when agents have different expertise levels
async def weighted_code_quality_assessment(code_file: str) -> AggregationResult:
"""Aggregate quality scores from agents with different weights."""
agents_and_weights = [
(senior_dev_agent, 0.5), # Senior dev opinion weighted highest
(style_checker_agent, 0.3), # Style matters but less than logic
(perf_agent, 0.2) # Performance is important but context-dependent
]
code_content = open(code_file).read()
results = await asyncio.gather(*[
agent.execute_async(task=f"Rate code quality (0-10):\n\n{code_content}")
for agent, _ in agents_and_weights
])
aggregator = WeightedAggregator(
weights=[weight for _, weight in agents_and_weights],
numeric_fields=["quality_score", "maintainability_score", "readability_score"]
)
return aggregator.aggregate([r.structured_output for r in results])
For more advanced patterns around orchestrating complex agent workflows, see our deep dive on AI agent workflows.
10. Using @Codex in Slack for Team Workflows
The Codex SDK ships with a first-class Slack integration that enables engineering teams to trigger multi-agent workflows directly from their communication platform. The @Codex bot can be mentioned in any channel to initiate tasks, and it reports progress and results back to the thread in real time.
Installing and Configuring the Slack Integration
pip install "openai-codex-sdk[slack]>=2.4.0"
from openai_codex.slack import CodexSlackBot, SlackBotConfig, SlackCommand
from openai_codex import AgentRegistry
# Configure the Slack bot
bot_config = SlackBotConfig(
bot_token=os.environ["SLACK_BOT_TOKEN"],
app_token=os.environ["SLACK_APP_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"],
# Which Slack channels the bot is active in
allowed_channels=["#engineering", "#code-review", "#devops"],
# Map Slack user IDs to Codex permission levels
user_permissions={
"U123456": "admin", # Can trigger any agent, modify configs
"U789012": "developer", # Can trigger code agents, cannot modify configs
"U345678": "viewer" # Can only view agent outputs, cannot trigger
},
# Cost limit per Slack-triggered run
per_request_cost_limit_usd=2.00,
# Require confirmation for expensive operations
confirmation_threshold_usd=1.00
)
# Register agents with the Slack bot
registry = AgentRegistry(client=client)
registry.register(code_writer)
registry.register(code_reviewer)
registry.register(bug_fixer)
bot = CodexSlackBot(config=bot_config, registry=registry)
# Define custom Slack commands
@bot.command("/codex-review")
async def handle_review_command(command: SlackCommand):
"""Handle /codex-review <PR_URL> command."""
pr_url = command.text.strip()
if not pr_url.startswith("https://github.com/"):
await command.respond("Please provide a valid GitHub PR URL.")
return
# Post initial acknowledgment
await command.respond(
f"🤖 Starting code review for {pr_url}...",
in_thread=True
)
# Extract PR details and run pipeline
pr_context = await extract_pr_context(pr_url)
pipeline = build_code_review_pipeline()
# Stream progress updates to Slack thread
async for progress_update in pipeline.execute_stream(pr_context):
if progress_update.stage_completed:
await command.respond(
f"✅ {progress_update.stage_name} complete "
f"({progress_update.duration_ms}ms)",
in_thread=True
)
# Post final review
result = pipeline.last_result
await command.respond(
blocks=format_review_as_slack_blocks(result.outputs["final_review"]),
in_thread=True
)
@bot.mention_handler
async def handle_mention(mention: SlackCommand):
"""Handle @Codex mentions in channels."""
message_text = mention.text.lower()
if "fix" in message_text and "bug" in message_text:
# Route to bug fixing workflow
await mention.respond("🔧 Initiating bug analysis...")
# Extract issue details from message context
issue_details = await extract_issue_from_message(mention)
await run_targeted_bug_fix(issue_details, mention)
elif "review" in message_text:
await mention.respond("👀 Starting code review workflow...")
# Extract code from message attachments or linked files
elif "explain" in message_text:
# Route to explanation agent (no code execution needed)
explanation_agent = Agent(
client=client,
name="explainer",
model="gpt-4.5",
system_prompt="Explain code clearly to engineering teams."
)
result = await explanation_agent.execute_async(task=mention.text)
await mention.respond(result.text, in_thread=True)
# Start the bot
if __name__ == "__main__":
bot.start() # Uses Socket Mode for real-time event handling
Configuring Slack Workflow Triggers
Beyond slash commands and mentions, the Codex Slack integration supports workflow triggers that activate agents based on channel events. For example, you can configure an agent to automatically run a security scan whenever a message containing a GitHub PR link is posted to #code-review.
from openai_codex.slack import ChannelEventTrigger, TriggerCondition
# Auto-trigger security scan on PR links
pr_link_trigger = ChannelEventTrigger(
name="auto_security_scan",
condition=TriggerCondition(
event_type="message",
channels=["#code-review"],
contains_pattern=r"github\.com/[^/]+/[^/]+/pull/\d+",
user_permission_required="developer"
),
action=lambda event: run_security_scan_for_pr(
pr_url=extract_github_url(event.text)
),
cooldown_seconds=60 # Don't trigger again for same PR within 60s
)
bot.register_trigger(pr_link_trigger)
11. Admin Tools for Managing Agent Permissions
In team environments, controlling what agents can access and execute is as important as the agents themselves. The Codex SDK provides a comprehensive RBAC system for agent permissions, an audit log for all agent actions, and administrative tools for managing agent configurations across an organization.
Defining Permission Policies
from openai_codex.admin import PermissionPolicy, AgentRole, ResourcePermission
from openai_codex.admin import PolicyEnforcer, AuditLogger
# Define roles with specific permissions
roles = {
"read_only_agent": AgentRole(
name="read_only_agent",
permissions=[
ResourcePermission(resource="filesystem", actions=["read"]),
ResourcePermission(resource="git", actions=["diff", "log", "show"]),
ResourcePermission(resource="shell", actions=[]), # No shell access
]
),
"code_writer_agent": AgentRole(
name="code_writer_agent",
permissions=[
ResourcePermission(
resource="filesystem",
actions=["read", "write"],
constraints={"allowed_paths": ["/workspace/src", "/workspace/tests"]}
),
ResourcePermission(
resource="shell",
actions=["execute"],
constraints={"allowed_commands": ["python", "pip", "pytest"]}
),
ResourcePermission(
resource="git",
actions=["diff", "add", "commit"],
constraints={"max_files_per_commit": 10}
)
]
),
"deployment_agent": AgentRole(
name="deployment_agent",
permissions=[
ResourcePermission(resource="filesystem", actions=["read"]),
ResourcePermission(
resource="shell",
actions=["execute"],
constraints={
"allowed_commands": ["docker", "kubectl", "helm"],
"require_approval_for": ["kubectl delete", "helm uninstall"]
}
),
ResourcePermission(
resource="secrets",
actions=["read"],
constraints={"allowed_secret_names": ["DEPLOY_KEY", "REGISTRY_TOKEN"]}
)
]
)
}
# Create and enforce policies
policy_enforcer = PolicyEnforcer(
roles=roles,
default_role="read_only_agent", # Agents default to minimal permissions
audit_logger=AuditLogger(
output="file",
file_path="/var/log/codex/agent_audit.jsonl",
include_payloads=True, # Log full tool call payloads
retention_days=90
)
)
# Assign roles to agents
policy_enforcer.assign_role("code_writer", "code_writer_agent")
policy_enforcer.assign_role("code_reviewer", "read_only_agent")
policy_enforcer.assign_role("deployment_agent", "deployment_agent")
# Attach enforcer to client - all agents automatically inherit policies
client.set_policy_enforcer(policy_enforcer)
Approval Workflows for High-Risk Operations
from openai_codex.admin import ApprovalWorkflow, ApprovalRequest, ApprovalChannel
# Configure approval workflow for dangerous operations
approval_workflow = ApprovalWorkflow(
client=client,
approval_channel=ApprovalChannel.SLACK,
slack_channel="#devops-approvals",
approvers=["U123456", "U234567"], # Slack user IDs of authorized approvers
approval_timeout_seconds=3600, # 1 hour to approve
# Define which operations require approval
requires_approval=[
"kubectl delete",
"helm uninstall",
"git push --force",
"DROP TABLE", # SQL operations matching this pattern
"rm -rf"
]
)
# Register with the policy enforcer
policy_enforcer.set_approval_workflow(approval_workflow)
# When an agent attempts a restricted operation, it automatically pauses
# and sends an approval request to Slack before proceeding
Viewing and Managing Active Agents
from openai_codex.admin import AgentManager
manager = AgentManager(client=client)
# List all currently running agents
active_agents = await manager.list_running()
# Illustrative admin API: not part of the documented Codex SDK
for agent in active_agents:
print(f"{agent.id}: task={agent.current_task!r}, elapsed={agent.elapsed_seconds:.1f}s")
if active_agents:
await manager.stop(active_agents[0].id)
The preceding block is illustrative, like the rest of section 11: the openai_codex.admin module, AgentManager, ApprovalWorkflow and the policy classes are not documented Codex SDK interfaces. The official Codex SDK documentation describes Codex and AsyncCodex clients, threads created with thread_start(), execution through run(), and sandbox presets. The next sections implement monitoring, budgets and error handling in your own orchestrator.
12. Monitoring Agent Execution
Monitoring should tell you which agent is running, how long it has been running, whether it finished, and what error ended the run. Keep that operational record outside the model response: a response may describe success while a later validation, file operation or deployment step fails.
The wrapper below uses the documented asynchronous thread flow and records lifecycle data in application-owned metrics, logs and run records. It needs the prometheus_client package (pip install prometheus-client); start_http_server serves the metrics over HTTP on the port set by METRICS_PORT, 9090 by default.
import asyncio
import logging
import os
import time
import uuid
from dataclasses import dataclass
from typing import Optional
from openai_codex import AsyncCodex, Sandbox
from prometheus_client import Counter, Gauge, Histogram, start_http_server
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("codex_orchestrator")
RUNS = Counter(
"codex_agent_runs_total",
"Agent runs by final status",
["agent", "status"],
)
DURATION = Histogram(
"codex_agent_duration_seconds",
"Agent execution duration",
["agent"],
)
ACTIVE = Gauge(
"codex_agents_active",
"Currently active agent runs",
["agent"],
)
@dataclass
class RunRecord:
run_id: str
agent: str
started_at: float
ended_at: Optional[float] = None
status: str = "running"
error: Optional[str] = None
class MonitoredCodexRunner:
def __init__(self, model: Optional[str] = None) -> None:
self.model = model
self.runs: dict[str, RunRecord] = {}
async def run(
self,
agent: str,
prompt: str,
sandbox: Sandbox = Sandbox.workspace_write,
) -> str:
run_id = str(uuid.uuid4())
record = RunRecord(run_id, agent, time.time())
self.runs[run_id] = record
started = time.monotonic()
ACTIVE.labels(agent).inc()
logger.info("agent_started agent=%s run_id=%s", agent, run_id)
try:
async with AsyncCodex() as codex:
options = {"sandbox": sandbox}
if self.model:
options["model"] = self.model
thread = await codex.thread_start(**options)
result = await thread.run(prompt)
record.status = "succeeded"
RUNS.labels(agent, "succeeded").inc()
return result.final_response
except asyncio.CancelledError:
record.status = "cancelled"
RUNS.labels(agent, "cancelled").inc()
logger.warning("agent_cancelled agent=%s run_id=%s", agent, run_id)
raise
except Exception as exc:
record.status = "failed"
record.error = str(exc)
RUNS.labels(agent, "failed").inc()
logger.exception(
"agent_failed agent=%s run_id=%s", agent, run_id
)
raise
finally:
record.ended_at = time.time()
DURATION.labels(agent).observe(time.monotonic() - started)
ACTIVE.labels(agent).dec()
logger.info(
"agent_finished agent=%s run_id=%s status=%s",
agent,
run_id,
record.status,
)
async def main() -> None:
start_http_server(int(os.getenv("METRICS_PORT", "9090")))
runner = MonitoredCodexRunner(model=os.getenv("CODEX_MODEL"))
response = await runner.run(
agent="reviewer",
prompt="Review the current workspace and report correctness risks.",
sandbox=Sandbox.read_only,
)
print(response)
if __name__ == "__main__":
asyncio.run(main())
MonitoredCodexRunner.runs keeps one RunRecord per run, with status, timestamps and any error, so you can serve it from an admin endpoint or write it to your database. The Prometheus metrics cover the aggregate view: runs by status, durations and active runs per agent.
Use stable, low-cardinality metric labels. An agent role such as reviewer is suitable; a run identifier, repository URL, or task text is not. Put unique identifiers in structured logs and correlate them with your own run record.
Prometheus metrics show what your orchestrator observed, but they do not replace account-level usage information. The Codex pricing and usage documentation states that the Codex command-line interface (CLI) /status command shows remaining limits. It also links to the Codex usage dashboard. Check both when local concurrency appears normal but new runs are being constrained.
13. Cost Management Across Multiple Agents
Do not estimate a multi-agent run from prompt length alone. OpenAI states that Codex usage depends on model choice, context, reasoning, tool use, retrieval, and caching. Task size and complexity, as well as whether work runs locally or in the cloud, also affect usage. Measure completed workflows and enforce limits at the orchestration boundary rather than assuming every worker consumes the same amount.
Authentication mode also changes the available workflow. When Codex is used with an API key, usage is billed according to API pricing and cloud-based features such as Slack are unavailable. The @Codex Slack workflow in section 10 therefore assumes ChatGPT-plan sign-in; with API-key authentication you would need to build your own Slack integration. These details are documented on the official Codex pricing page.
Fast mode is not a free latency switch: OpenAI documents that it consumes limits at higher rates. Make it an explicit configuration choice for a specific workflow instead of enabling it globally.
Apply several independent controls. Bound concurrency so a large queue cannot start every agent at once. Give each run a wall-clock deadline and a maximum number of orchestration turns. Keep tasks tightly scoped, select a smaller configured model for routine subtasks when it produces a useful result, and give review-only agents Sandbox.read_only rather than write access.
import asyncio
import os
from collections.abc import Sequence
from openai_codex import AsyncCodex, Sandbox
async def run_bounded(
tasks: Sequence[str],
max_concurrency: int = 3,
timeout_seconds: float = 180.0,
) -> list[str | BaseException]:
semaphore = asyncio.Semaphore(max_concurrency)
model = os.getenv("CODEX_MODEL")
async with AsyncCodex() as codex:
async def run_one(task: str) -> str:
async with semaphore:
options = {"sandbox": Sandbox.read_only}
if model:
options["model"] = model
thread = await codex.thread_start(**options)
result = await asyncio.wait_for(
thread.run(task),
timeout=timeout_seconds,
)
return result.final_response
results = await asyncio.gather(
*(run_one(task) for task in tasks),
return_exceptions=True,
)
return list(results)
The sample needs no turn counter because every task makes exactly one thread.run() call. If a workflow continues a thread across multiple turns, increment an application-owned counter before each call and reject the next call when it would exceed the run’s configured turn budget.
Inspect each returned item and classify exceptions as timed out, canceled, or failed before persisting the batch result. Because return_exceptions=True keeps one failure from terminating collection, successful sibling results remain available for validation or aggregation.
A semaphore limits simultaneous work, while asyncio.wait_for() limits the duration of each task. For a queue-backed service, also cap queued work per repository or team so one large request cannot crowd out unrelated work. Track accepted and validated results, not just the number of tasks started.
14. Error Handling and Fallback Strategies
Failures should remain local to the agent that caused them. Wrap each invocation with asyncio.wait_for(), record timeouts separately from other failures, and cancel dependent work when its required input is unavailable. Independent agents may continue, but the final aggregator must know which inputs are missing.
Retry only failures your application has classified as transient, such as a temporary transport interruption. Use exponential backoff with jitter and a small attempt limit. Catch a generic Exception at the SDK boundary, preserve the original exception in logs, and avoid inventing SDK-specific exception classes.
import asyncio
import random
from collections.abc import Awaitable, Callable
from typing import TypeVar
T = TypeVar("T")
async def retry_transient(
operation: Callable[[], Awaitable[T]],
is_transient: Callable[[Exception], bool],
attempts: int = 3,
) -> T:
for attempt in range(attempts):
try:
return await operation()
except Exception as exc:
if not is_transient(exc) or attempt == attempts - 1:
raise
delay = (2 ** attempt) + random.uniform(0.0, 0.5)
await asyncio.sleep(delay)
raise RuntimeError("retry loop ended unexpectedly")
Do not blindly retry non-idempotent work. A task that edits files, creates a pull request, sends a message, or starts a deployment may have completed its side effect before the caller lost the response. Assign an operation identifier and check for the existing side effect before executing the operation again.
- Isolate writes: give each writing agent its own Git branch or worktree. Validate the diff and discard the worktree when the run fails.
- Stop on conflicts: if agents modify overlapping lines, require a reconciliation step or human review instead of choosing a patch silently.
- Reduce scope safely: if a specialist times out, use a single-agent fallback only for low-risk work that remains useful with reduced coverage.
- Fail closed: send credentials, destructive commands, production changes, repeated validation failures, and unresolved conflicts to a human reviewer.
A nonempty final_response shows that a thread returned text. Treat that text as a proposed result until separate checks establish that generated code compiles, tests pass, patches apply, and required approvals exist.
15. Conclusion and Next Steps
A maintainable multi-agent coding system is an orchestrator around narrowly scoped Codex threads. It controls concurrency, selects a sandbox, records state transitions, validates artifacts outside the model, and places approval checks before risky side effects. Coordinator, pipeline, and swarm patterns remain useful designs, but the application must implement them using documented SDK primitives.
Keep model selection in configuration. OpenAI states that GPT-5.5 retires from ChatGPT, ChatGPT Work, and Codex on all plans on October 14, 2026, while the OpenAI API is not affected. Before selecting or migrating a model, check the official Codex models page and confirm that the configured model is available to the account running the workflow.
- Replace illustrative administration: inventory the illustrative
openai_codex.admin, permission-policy, and approval-workflow calls in section 11. Replace them with application-owned authorization, tracking, and approval components around documented thread operations. - Add a continuous integration entry point: connect one bounded workflow through the documented Codex GitHub Action or non-interactive mode, then make its validation result visible in the build.
- Create replay tests: save representative task specifications and expected invariants, then rerun them after prompt, model, permission, or orchestration changes.
- Gate write access: implement identity checks, approval records, and exact diff or command previews before enabling repository writes or deployment tools.
Start by replacing one read-only illustrative workflow with documented AsyncCodex, thread_start(), and run() calls. Add metrics and validation before expanding it to concurrent writers.
