CrewAI Multi-Agent Architecture
A 3-agent pipeline that takes a plain-English task description and ships code — Google Gemini 3.0 Pro plans it, Claude Opus 4.6 implements it via an OAuth proxy, and OpenAI o3 reviews it. Two Python files, zero YAML configs.
The Agent Team
Three agents defined in dev_team.py. Each uses a different LLM, chosen for what it's actually good at — not marketing benchmarks.
PM Agent
Google Gemini 3.0 ProArchitects the solution. Reads the project structure, analyzes existing source files, and produces a file-level implementation plan with sub-tasks, dependencies, and acceptance criteria. Uses gemini/gemini-3-pro-preview via Google AI Studio. Has full tool access (shell, file read/write, grep) to explore the codebase before planning.
Dev Agent
Claude Opus 4.6Runs via our local OAuth proxy (claude_proxy.py on port 5111) to access full Claude Code CLI capabilities. Has zero CrewAI tools — the proxy runs an internal agentic loop where Claude calls shell_command, write_file, edit_file, and others directly. CrewAI only sees the final text summary.
Reviewer Agent
OpenAI o3Performs deep code review using openai/o3 with full tool access. Reads the PM's plan as its checklist, then independently verifies the implementation — reading files, running builds, grepping for regressions. Returns APPROVE or REQUEST CHANGES with specific line-level feedback.
Why We Built an OAuth Proxy
The hard part wasn't CrewAI orchestration — it was getting Claude Opus 4.6 to work as a CrewAI agent at all.
Problem 1: No API Key for Claude Code
Claude Code (the CLI) uses OAuth authentication stored in macOS Keychain, not standard Anthropic API keys. If you have a Claude Pro/Max subscription, your access token lives in Keychain under the service name Claude Code-credentials. There's no way to pass this to CrewAI's standard LiteLLM integration — it expects an ANTHROPIC_API_KEY environment variable.
Problem 2: CrewAI's Parser Fragility
When Claude uses tools through CrewAI's native tool system, CrewAI has to parse the tool calls and results from the LLM response. This parser frequently breaks on complex outputs — HTML files with special characters, large file contents, nested JSON. The errors are cryptic (OutputParserException) and unrecoverable.
Solution: Local Agentic Proxy
claude_proxy.py is a ~300-line Python HTTP server that presents an OpenAI-compatible /v1/chat/completions endpoint on localhost:5111. Internally, it reads the OAuth token from Keychain, calls the Anthropic API directly with tool definitions, and runs a full agentic loop — Claude calls tools, the proxy executes them locally, feeds results back to Claude, repeat for up to 100 iterations. CrewAI only receives the final text summary. No parser issues. No token leaks.
def get_token(): """Read Claude OAuth token from macOS Keychain.""" with _token_lock: now = time.time() if _token_cache["token"] and _token_cache["expires"] > now: return _token_cache["token"] result = subprocess.run( ["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"], capture_output=True, text=True ) if result.returncode != 0: raise RuntimeError("Cannot read Keychain") data = json.loads(result.stdout.strip()) token = data.get("claudeAiOauth", {}).get("accessToken", "") _token_cache["token"] = token _token_cache["expires"] = now + 300 # Cache 5 min return tokendef run_agent_loop(messages, model, system=None, max_tokens=16384): """Claude calls tools → proxy executes locally → feed results back.""" for iteration in range(1, 101): # Up to 100 iterations resp = call_anthropic(messages, model, system, max_tokens) tool_uses = [b for b in resp["content"] if b["type"] == "tool_use"] # If no tool calls → Claude is done. Return final text. if not tool_uses or resp.get("stop_reason") != "tool_use": text = "\n".join(b.get("text", "") for b in resp["content"] if b["type"] == "text") return text, total_input, total_output # Execute each tool call locally messages.append({"role": "assistant", "content": resp["content"]}) tool_results = [] for tu in tool_uses: result = execute_tool(tu["name"], tu["input"]) # Runs on local machine tool_results.append({ "type": "tool_result", "tool_use_id": tu["id"], "content": result }) messages.append({"role": "user", "content": tool_results}) # Loop continues — Claude sees results, may call more toolsAgent Definitions in Code
No YAML configs — agents are defined directly in dev_team.py using the CrewAI Python API.
# 🧠 PM — Google Gemini 3.0 Pro pm = Agent( role="Project Manager & Architect", goal="Break down tasks into clear implementation plans with specific file paths and acceptance criteria", backstory="Senior tech lead with 15 years experience...", llm="gemini/gemini-3-pro-preview", tools=ALL_TOOLS, # shell, read, write, list, search allow_delegation=False, verbose=True, ) # 💻 DEV — Claude Opus 4.6 (via agentic proxy) claude_llm = LLM( model="openai/claude-opus-4-6", base_url="http://localhost:5111/v1", api_key="not-needed", # Auth via Keychain, not API key ) dev = Agent( role="Senior Full-Stack Developer", goal="Write clean, well-tested code following best practices", backstory="Expert Flutter/Dart and web developer...", llm=claude_llm, tools=[], # No CrewAI tools — proxy handles everything verbose=True, ) # 🔍 REVIEWER — OpenAI o3 reviewer = Agent( role="Senior Code Reviewer & QA Engineer", goal="Review code for bugs, security issues, performance", backstory="Meticulous code reviewer who catches issues others miss...", llm="openai/o3", tools=ALL_TOOLS, verbose=True, )Key detail: The Dev Agent has tools=[]. This is intentional. Claude Opus 4.6 doesn't use CrewAI's tool layer at all — it calls tools inside the proxy's agentic loop. CrewAI sends it the task, the proxy runs Claude with real filesystem tools for up to 100 iterations, and CrewAI gets back a plain text summary. This completely sidesteps CrewAI's parser.
Prerequisites
What you actually need on your machine to run this.
🐍 Runtime
- Python 3.13+
- crewai[tools] package
- pydantic, urllib3
🖥️ Platform
- macOS (Keychain required for proxy)
- Claude Code CLI installed & logged in
- Stable internet for API calls
🔑 API Keys & Auth
- GOOGLE_API_KEY (Gemini 3.0 Pro)
- Claude Code OAuth (macOS Keychain)
- OPENAI_API_KEY (OpenAI o3)
Running the Team
Two terminals. One command each.
# Start the Claude OAuth proxy (must be running before dev_team.py) cd ~/clawd/crewai-team CREWAI_PROJECT=~/Desktop/examcertwebsite python3 claude_proxy.py # Output: # [proxy] ✅ Token: eyJhbGciOiJSUzI1... # [proxy] 📂 Project: /Users/you/Desktop/examcertwebsite # [proxy] 🔧 Tools: shell_command, read_file, write_file, edit_file, list_directory, search_files # [proxy] 🧠 Extended thinking + 100 iterations # [proxy] 🚀 Claude Code Proxy on http://127.0.0.1:5111# Run the 3-agent team with a task cd ~/clawd/crewai-team CREWAI_PROJECT=~/Desktop/examcertwebsite \ python3 dev_team.py "Fix the login page layout and add form validation" # Output: 🚀 Starting AI Dev Team... 📋 Task: Fix the login page layout and add form validation 📂 Project: ~/Desktop/examcertwebsite 🧠 PM: Gemini 3.0 Pro 💻 Dev: Claude Opus 4.6 (agentic proxy) 🔍 Reviewer: OpenAI o3 ============================================================ # PM explores the codebase, creates an implementation plan # Dev receives the plan, proxy runs Claude with filesystem tools # Reviewer verifies the changes, returns APPROVE or REQUEST CHANGESThe CREWAI_PROJECT environment variable sets the working directory for all tool calls. Every file path is resolved relative to this root. Logs are written to ~/clawd/crewai-team/logs/ in real-time — you can tail -f them to watch the agentic loop progress.
Tool Architecture
Tools exist in two layers: CrewAI-managed (for PM & Reviewer) and proxy-internal (for Dev).
🔧 shell_command
Executes arbitrary bash commands in the project root. 300s timeout. Used for node build.js, git commit, npm install, etc. Output truncated to 20K chars.
📖 read_file
Reads file contents with optional offset + limit for line-range selection. Proxy version supports up to 50K chars. CrewAI version truncates at 20K.
✏️ write_file
Writes complete file content, creating parent directories automatically with os.makedirs. Both layers use the same logic.
🔧 edit_file
Surgical find-and-replace: provide old_text (exact match) and new_text. Uses str.replace(old, new, 1) — first occurrence only. Fails explicitly if old_text not found.
📂 list_directory
Lists files and directories, optionally recursive. Proxy version uses os.walk with smart exclusions (node_modules, __pycache__, .git). Capped at 500 entries.
🔍 search_files
Runs grep -rn with --include glob filtering. Supports case-insensitive flag. Results truncated at 15K chars (proxy) or 10K (CrewAI).
How the Proxy Bridges CrewAI → Anthropic
The data flow from CrewAI's perspective vs. what actually happens.
┌─────────────────────────────────────────────────────────┐ │ CrewAI Orchestrator (dev_team.py) │ │ │ │ PM Agent ──→ gemini/gemini-3-pro-preview (Google API) │ │ Reviewer ──→ openai/o3 (OpenAI API) │ │ │ │ Dev Agent ──→ openai/claude-opus-4-6 │ │ base_url=http://localhost:5111/v1 │ └──────────────────────────┬──────────────────────────────┘ │ POST /v1/chat/completions (OpenAI-compatible format) │ ┌──────────────────────────▼──────────────────────────────┐ │ claude_proxy.py (localhost:5111) │ │ │ │ 1. Extract messages from OpenAI format │ │ 2. Read OAuth token from macOS Keychain │ │ 3. Call Anthropic API (claude-opus-4-6) │ │ with tools: shell, read, write, edit, list, search │ │ 4. ┌──── Agentic Loop (up to 100 iterations) ────┐ │ │ │ Claude returns tool_use blocks │ │ │ │ → Proxy executes tools on local filesystem │ │ │ │ → Results fed back to Claude │ │ │ │ → Repeat until stop_reason != "tool_use" │ │ │ └──────────────────────────────────────────────┘ │ │ 5. Return final text as OpenAI chat.completion │ └─────────────────────────────────────────────────────────┘Why openai/claude-opus-4-6 as the model string? CrewAI uses LiteLLM under the hood. The openai/ prefix tells LiteLLM to use the OpenAI-compatible client, which just needs a base_url. The model name itself is passed through to our proxy, which strips the prefix and calls Anthropic's API with the real model ID claude-opus-4-6. The api_key="not-needed" is a dummy — auth happens via Keychain inside the proxy.
req = urllib.request.Request( "https://api.anthropic.com/v1/messages", data=json.dumps(req_body).encode(), headers={ "Authorization": f"Bearer {token}", # OAuth token from Keychain "anthropic-version": "2023-06-01", "anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14", "user-agent": "claude-cli/2.1.32 (external, cli)", "x-app": "cli", # Required for Claude Code subscription billing } )The beta flags are critical: claude-code-20250219 enables the Claude Code tool behavior, oauth-2025-04-20 allows OAuth bearer tokens (instead of x-api-key), and interleaved-thinking-2025-05-14 enables extended thinking between tool calls. The user-agent must match the CLI signature for the subscription to be billed correctly.
Project Structure
The entire system is two files plus a logs directory.
crewai-team/ ├── dev_team.py # CrewAI orchestrator: agents, tasks, tools, crew ├── claude_proxy.py # OAuth proxy: Keychain auth, agentic loop, tool execution └── logs/ # Per-run markdown logs (auto-created) ├── run-20250627-143052.md └── crew-20250627-143052.mdThere's no agents.yaml, no tasks.yaml, no crew.py scaffold. We skipped crewai create entirely and defined everything in pure Python. Agents, tasks, and the crew are all in dev_team.py. The proxy is a standalone HTTP server that runs independently.
What We Learned
Hard-won lessons from running this in production on real codebases.
1 CrewAI's Parser Breaks on Real Code
Any tool output containing HTML, JSON, or special characters will eventually cause an OutputParserException. The proxy pattern (tools inside the LLM loop, plain text out) eliminates this entirely for the Dev agent.
2 Match the Model to the Task
Google Gemini 3.0 Pro is fast and cheap for planning (large context window, good at structured output). Claude Opus 4.6 excels at code generation with tool use. OpenAI o3 is thorough for adversarial review. Don't use one model for everything.
3 OAuth Tokens Expire
The Keychain token from Claude Code is cached for 5 minutes in the proxy. It can expire mid-run on long tasks. The proxy auto-refreshes, but if Claude Code hasn't been used recently, you may need to run claude once to re-auth.
4 Dev Gets Zero CrewAI Tools
Setting tools=[] on the Dev agent is critical. If you give it CrewAI tools AND proxy tools, it tries to use both and gets confused. The proxy handles everything — Claude calls tools, proxy executes, Claude sees results.
5 Sequential Process, Not Hierarchical
CrewAI supports hierarchical delegation, but it's unreliable when agents have different LLM backends. Sequential (Process.sequential) is predictable: PM plans → Dev implements → Reviewer verifies. Each task fully completes before the next starts.
6 Log Everything in Real-Time
The proxy writes incremental markdown logs to ~/clawd/crewai-team/logs/ on every tool call. When a run goes wrong at iteration 47, you need to see exactly which file write or shell command failed. tail -f is your friend.
See What This Pipeline Ships
This page was built by the same CrewAI pipeline described above. Browse our certification guides to see more of its output.
