Different perspectives. Same thing being built.
One of the most useful setups in production: agents that all look at the same thing at the same time, each one bringing a different perspective. It's not a pipeline (there's no fixed order). It's not a swarm (there's no chaos). It's "fan out, then merge": send the work to several agents at once, then combine their answers.
The product-build pattern
Picture five agents looking at a new feature proposal at the same time, each in its own thread, each producing its own structured response. None of them wait on the others. A coordinator agent then merges their outputs into one final document.
Why parallel-with-merge beats sequential
- Wall time = max(agents), not sum. Five agents at 8 seconds each = 8 seconds total, not 40.
- Independent perspectives aren't biased by each other's output. The security agent doesn't anchor on what the frontend agent already wrote.
- Conflicts surface naturally. When frontend says "session in localStorage" and security says "no PII in localStorage", you have a productive disagreement to resolve, not a quietly merged compromise.
- Each agent can fail independently. If devops times out, you still have four perspectives instead of zero.
Implementing parallel collaboration in Python
import asyncio
from pydantic import BaseModel
from typing import Literal
# ─── Each POV agent emits a TYPED contribution ───
class FrontendView(BaseModel):
ux_flow: list[str]
accessibility_notes: list[str]
components_needed: list[str]
class BackendView(BaseModel):
api_endpoints: list[dict]
data_model_changes: list[str]
migration_strategy: str
class SecurityView(BaseModel):
threat_model: list[dict] # [{threat, severity, mitigation}]
authz_rules: list[str]
data_classification: Literal["public", "internal", "sensitive"]
class DevOpsView(BaseModel):
rollout_plan: str
metrics_to_emit: list[str]
rollback_strategy: str
class ProductView(BaseModel):
user_stories: list[str]
success_metric: str
out_of_scope: list[str]
class FeatureSpec(BaseModel):
name: str
frontend: FrontendView
backend: BackendView
security: SecurityView
devops: DevOpsView
product: ProductView
conflicts: list[dict] = [] # surfaced disagreements
# ─── Parallel build with per-agent timeout ───
async def _run_with_timeout(name, coro, timeout_s):
# Wrap a single agent call so one slow agent can't block the others.
try:
return name, await asyncio.wait_for(coro, timeout=timeout_s)
except asyncio.TimeoutError:
return name, None # agent missed the window
except Exception as e:
return name, {"error": str(e)} # surface, don't crash
async def build_feature_spec(feature_brief: str, timeout_s: float = 15) -> FeatureSpec:
tasks = {
"frontend": frontend_agent.run(feature_brief),
"backend": backend_agent.run(feature_brief),
"security": security_agent.run(feature_brief),
"devops": devops_agent.run(feature_brief),
"product": product_agent.run(feature_brief),
}
# gather runs all coroutines in parallel; each has its own timeout wrapper
pairs = await asyncio.gather(
*[_run_with_timeout(name, coro, timeout_s) for name, coro in tasks.items()]
)
results = dict(pairs)
# ─── MERGE step: detect conflicts, build canonical spec ───
spec = merge_views(feature_brief, results)
return spec
def merge_views(brief, views) -> FeatureSpec:
conflicts = []
# Cross-check 1: storage mentions vs data classification
if views["frontend"] and views["security"]:
ux = " ".join(views["frontend"].ux_flow).lower()
if "localstorage" in ux and views["security"].data_classification == "sensitive":
conflicts.append({
"type": "storage_classification",
"frontend": "uses localStorage",
"security": "data classified sensitive",
"resolution_required": True,
})
# Cross-check 2: API endpoints vs auth rules
if views["backend"] and views["security"]:
endpoints = {ep["path"] for ep in views["backend"].api_endpoints}
guarded = {r.split()[-1] for r in views["security"].authz_rules}
ungoverned = endpoints - guarded
if ungoverned:
conflicts.append({
"type": "missing_authz",
"endpoints": list(ungoverned),
})
return FeatureSpec(
name=brief,
frontend=views["frontend"] or _empty_view(FrontendView),
backend=views["backend"] or _empty_view(BackendView),
security=views["security"] or _empty_view(SecurityView),
devops=views["devops"] or _empty_view(DevOpsView),
product=views["product"] or _empty_view(ProductView),
conflicts=conflicts,
)
Two things in this code worth noticing: (1) each agent fills in its own typed section, so if one agent goes off the rails, it can only mess up its own field instead of corrupting the whole document. (2) The merge step actively looks for conflicts instead of just concatenating outputs. The list of conflicts ships along with the document, so a human or a higher-level agent has to deal with them on purpose.