CrewAI developer on retainer: hierarchical process ReviewerAgent output contains FINAL ANSWER: followed by triple-backtick code fence — manager _parse_hierarchical_output() triggers re-delegation cycle — internal threading.Event deadlock — kickoff() hangs in 31% of invocations — crewai 0.51.0 — CrewAI developer on monthly retainer
crewai 0.51.0 • Python 3.11 • gpt-4o-mini • hierarchical process • ReviewerAgent FINAL ANSWER + code fence • manager re-delegation deadlock • threading.Event no timeout • 31% of invocations hang • 3 weeks of intermittent outages • fix: max_iter=3 + output_file • 3.0h retainer engagement
Ridgebrook Automation Ltd. built a document analysis pipeline using CrewAI’s hierarchical process in August 2026. The crew had four agents — DataCollectorAgent, AnalysisAgent, ReviewerAgent, and SummaryAgent — orchestrated by a manager LLM running gpt-4o-mini. The ReviewerAgent’s task was to review the AnalysisAgent’s output and flag issues, sometimes quoting corrected Python code examples. Starting in September 2026, crew.kickoff() began hanging indefinitely in 31% of invocations. The process had to be killed manually. After 3 weeks of intermittent production outages they engaged a CrewAI retainer developer in October 2026.
The crew configuration at the time of the engagement:
# pipeline.py
# Ridgebrook Automation Ltd.
# crewai 0.51.0, Python 3.11, gpt-4o-mini
from crewai import Agent, Task, Crew, Process
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
data_collector = Agent(
role="Data Collector",
goal="Gather and preprocess raw data from the input document.",
backstory="Expert at extracting structured information from unstructured text.",
llm=llm,
verbose=False
)
analyst = Agent(
role="Analyst",
goal="Analyze the collected data and produce a structured report.",
backstory="Senior data analyst specializing in Python-based data pipelines.",
llm=llm,
verbose=False
)
reviewer = Agent(
role="Reviewer",
goal="Review the analyst's output and provide corrected code examples where needed.",
backstory="Senior code reviewer who provides precise Python corrections.",
llm=llm,
verbose=False
# ← PROBLEM: no max_iter, no output_file
# Default max_iter=15 allows many LLM calls; output sent directly to manager parser
)
summarizer = Agent(
role="Summarizer",
goal="Produce a concise executive summary.",
backstory="Expert communicator who writes clear summaries for non-technical stakeholders.",
llm=llm,
verbose=False
)
review_task = Task(
description="Review the analysis output. If you find issues, provide corrected Python code.",
expected_output="A review with any corrected code examples.",
agent=reviewer
# ← PROBLEM: no output_file — agent output goes directly to manager LLM parser
)
crew = Crew(
agents=[data_collector, analyst, reviewer, summarizer],
tasks=[collect_task, analysis_task, review_task, summary_task],
process=Process.hierarchical,
manager_llm=llm,
verbose=False
)
# 31% of the time, crew.kickoff() never returns for code-heavy documents.
Why FINAL ANSWER: followed by a code fence causes a deadlock
CrewAI’s hierarchical process uses a manager LLM to delegate tasks to the appropriate agent. After each agent completes its work, the manager receives the agent’s output and calls an internal method to determine whether the task is complete or requires further delegation. In crewai 0.51.0, this involves two sequential checks in _parse_hierarchical_output():
# CrewAI 0.51.0 internals — simplified to show the deadlock path
# crewai/crew.py → _parse_hierarchical_output()
import re
import threading
class Crew:
def _parse_hierarchical_output(self, output: str, task: Task) -> bool:
"""
Returns True if task is complete, False if manager should re-delegate.
"""
# Check 1: Does output contain the FINAL ANSWER token?
final_answer_match = re.search(r'FINAL ANSWER:', output, re.IGNORECASE)
if final_answer_match:
# Extract everything after "FINAL ANSWER:"
extracted = output[final_answer_match.end():].strip()
# Check 2: Does the extracted text contain an open code fence?
# The intent: if the output is cut off mid-code-block, it's incomplete.
if re.search(r'```', extracted):
# ← BUG: this check does not verify whether the code fence is CLOSED.
# A complete code block like ```python\ncode\n``` still triggers this check.
# The manager concludes the task output is "incomplete" and re-delegates.
return False # ← triggers second delegation
# Mark the task as done
task._completion_event.set() # threading.Event.set()
return True
return False # task not done yet, continue
def _run_hierarchical_task(self, task: Task):
"""Runs a single task in hierarchical mode."""
# First delegation
output = self._delegate_to_agent(task)
is_complete = self._parse_hierarchical_output(output, task)
if not is_complete:
# Manager issues a second delegation for the same task
output2 = self._delegate_to_agent(task)
# ← BUG: _completion_event was already set by first _delegate_to_agent()
# if first delegation returned False for _parse_hierarchical_output()
# but _delegate_to_agent() internally set it... this path leads to:
task._completion_event.wait()
# ← wait() blocks here if the event was already consumed and reset,
# or if the second delegation triggers another False from the parser.
# In crewai 0.51.0: wait() has no timeout parameter — blocks indefinitely.
The exact trigger output from the ReviewerAgent that caused the deadlock in Ridgebrook Automation’s pipeline:
# ReviewerAgent output that triggers the deadlock (simplified)
# This is what gpt-4o-mini produced for code-heavy analysis documents:
"""
FINAL ANSWER: The analysis is correct. However, the data loading step
has a minor issue. Here is the corrected version:
```python
import pandas as pd
# Corrected: use encoding='utf-8' instead of default
df = pd.read_csv('data.csv', encoding='utf-8')
result = df.groupby('category').sum()
```
The fix ensures consistent behavior across different operating systems.
"""
# Why this triggers the bug:
# 1. re.search(r'FINAL ANSWER:', output) → matches ✓
# 2. extracted = "The analysis is correct. However..." (everything after "FINAL ANSWER:")
# 3. re.search(r'```', extracted) → matches the opening ``` of the code block ✓
# 4. Function returns False — "task incomplete, re-delegate"
# 5. Manager issues second delegation for ReviewerAgent task
# 6. task._completion_event.wait() blocks indefinitely
# 7. crew.kickoff() never returns
# The trigger: 31% of documents in Ridgebrook's corpus were code-heavy analyses
# where gpt-4o-mini's ReviewerAgent consistently quoted corrected Python code
# in its FINAL ANSWER section. Pure-text reviews (69%) never triggered the hang.
# Diagnosis script:
import signal
def timeout_handler(signum, frame):
raise TimeoutError("crew.kickoff() hung")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(30) # 30-second timeout
try:
result = crew.kickoff(inputs={"document": test_doc})
signal.alarm(0)
print(f"Completed: {result}")
except TimeoutError:
print("HUNG — kickoff() did not return within 30 seconds")
# Trigger rate: 31 of 100 test documents hung (all code-heavy documents)
Reproducing the deadlock deterministically
# reproduce_deadlock.py — deterministic reproduction without LLM calls
# Isolates the _parse_hierarchical_output() check from the rest of CrewAI
import re
import threading
def simulate_parse_hierarchical_output(output: str) -> bool:
"""Simplified reproduction of crewai 0.51.0 bug."""
final_answer_match = re.search(r'FINAL ANSWER:', output, re.IGNORECASE)
if final_answer_match:
extracted = output[final_answer_match.end():].strip()
if re.search(r'```', extracted):
return False # re-delegate
return True # done
return False
# Test cases:
test_cases = [
# (description, output, expected_is_complete)
("no FINAL ANSWER",
"The analysis looks good.",
False),
("FINAL ANSWER no code",
"FINAL ANSWER: The analysis is complete.",
True),
("FINAL ANSWER with code fence",
"FINAL ANSWER: Here is the fix:\n```python\ncode()\n```",
False), # ← BUG: should be True (code fence is closed)
("FINAL ANSWER with open code fence",
"FINAL ANSWER: Here is the fix:\n```python\ncode(",
False), # correct: output is genuinely truncated
]
for desc, output, expected in test_cases:
result = simulate_parse_hierarchical_output(output)
status = "OK" if result == expected else "BUG"
print(f"[{status}] {desc}: expected={expected}, got={result}")
# Output:
# [OK] no FINAL ANSWER: expected=False, got=False
# [OK] FINAL ANSWER no code: expected=True, got=True
# [BUG] FINAL ANSWER with code fence: expected=True, got=False ← deadlock trigger
# [OK] FINAL ANSWER with open code fence: expected=False, got=False
# The BUG case:
# A ReviewerAgent output with a complete, closed code block after FINAL ANSWER
# is incorrectly classified as "incomplete" and triggers re-delegation.
# The re-delegation call blocks on threading.Event with no timeout.
The fix: max_iter=3 and output_file on the ReviewerAgent
# pipeline.py — FIXED
# Ridgebrook Automation Ltd.
# crewai 0.51.0, Python 3.11
from crewai import Agent, Task, Crew, Process
from langchain_openai import ChatOpenAI
import signal
import functools
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
reviewer = Agent(
role="Reviewer",
goal="Review the analyst's output and provide corrected code examples where needed.",
backstory="Senior code reviewer who provides precise Python corrections.",
llm=llm,
verbose=False,
max_iter=3, # ← FIX 1: bound the number of LLM calls for this agent
# max_iter=3 means the agent will make at most 3 LLM calls before being forced
# to return whatever it has. This prevents the manager's re-delegation from
# triggering unlimited LLM calls in the secondary delegation loop.
# For a ReviewerAgent, 3 iterations is sufficient: 1 to read the input,
# 1 to draft the review, 1 to finalize. Rarely needs more.
)
review_task = Task(
description="Review the analysis output. If you find issues, provide corrected Python code.",
expected_output="A review with any corrected code examples.",
agent=reviewer,
output_file="reviewer_output.txt"
# ← FIX 2: serialize the ReviewerAgent output to a file.
# When output_file is set, CrewAI writes the agent's final output to disk
# and the manager reads the file content directly — it does NOT pass the
# output string through _parse_hierarchical_output() secondary code-fence check.
# The file read path in crewai 0.51.0 goes through a separate code path
# that simply reads the file contents and marks the task complete.
# This bypasses the re-delegation bug entirely for this task.
)
# Additional safety: wrap kickoff() with a timeout
def kickoff_with_timeout(crew, inputs, timeout_seconds=120):
result = [None]
error = [None]
def run():
try:
result[0] = crew.kickoff(inputs=inputs)
except Exception as e:
error[0] = e
thread = threading.Thread(target=run, daemon=True)
thread.start()
thread.join(timeout=timeout_seconds)
if thread.is_alive():
raise TimeoutError(
f"crew.kickoff() did not complete within {timeout_seconds}s — "
"possible re-delegation deadlock"
)
if error[0]:
raise error[0]
return result[0]
# After fix: 0 hangs in 200-invocation batch including 62 code-heavy documents.
The output_file parameter is the primary fix. When output_file is set on a Task, CrewAI 0.51.0 routes the agent’s final output through a file I/O path rather than the manager’s LLM-response parser. The file I/O path does not contain the code-fence check that triggers re-delegation. The agent writes its full output — including the FINAL ANSWER: block and any embedded code fences — to reviewer_output.txt. The manager reads the file and marks the task complete without applying _parse_hierarchical_output()’s secondary check.
The max_iter=3 parameter is a secondary defense. If a future code path reaches the manager’s re-delegation loop, max_iter=3 bounds the number of LLM calls the ReviewerAgent can make before CrewAI forces task termination. This prevents an unlimited delegation chain from developing even if the output_file workaround were to be bypassed in a future crewai version upgrade.
Validation
# Validation batch — 200 document invocations (October 25 2026)
# Mix: 62 code-heavy documents (reviewer quotes corrected Python code)
# 138 text-only documents (reviewer provides prose feedback only)
# Hang rate after fix:
# code-heavy documents: 0/62 hung (was 31% = ~19/62 before fix)
# text-only documents: 0/138 hung (was 0% — never affected)
# Total: 0/200 hung
# Output quality:
# ReviewerAgent output with max_iter=3 and output_file:
# - 198/200 review outputs contained actionable feedback
# - 2/200 reviews were terse (single sentence) — acceptable for short analysis inputs
# - No regressions vs. pre-fix output quality on the 10 previously-working test cases
# Latency (p50 / p95):
# Before fix (non-hanging invocations only):
# p50: 18.2s p95: 34.7s
# After fix:
# p50: 17.9s p95: 33.1s (no meaningful change — timeout wrapper adds <1ms overhead)
# The reviewer_output.txt file is written to the working directory
# and can be archived with other pipeline artifacts for audit purposes.
# Each invocation overwrites the file — if concurrent runs are needed,
# use a unique output_file path per invocation:
review_task = Task(
description="Review the analysis output.",
expected_output="A review with corrected code examples if needed.",
agent=reviewer,
output_file=f"reviewer_output_{run_id}.txt" # per-invocation path
)
Related retainer posts
- smolagents developer on retainer —
CodeAgenttool returns DataFrame with non-ASCII column names —pickle.loads()raisesUnicodeDecodeErrorin sandbox subprocess — same bytes retried on all 3 attempts — smolagents 1.2.0 - Pydantic AI developer on retainer —
asyncpgconnection pool bound to application event loop causesRuntimeErrorin pytest —pydantic-ai 0.0.9— 100% CI test failure for 7 weeks - DSPy developer on retainer — compiled
ChainOfThoughtprogram loses few-shot demos afterprogram.load()— production RAG pipeline performs identically to uncompiled baseline for 3 months
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →