Solutions — Three Extensions of the Brief Crew
Lesson 10 · Building Your Own Agents · worked answers with commentary
Below is one clean answer per homework task, plus the why-this-not-that commentary that distinguishes a passing solution from an idiomatic CrewAI one. Compare your attempt against these — even a different-but-working solution is fine, but the commentary often surfaces traps you wouldn’t notice from a successful run alone.
How to read the solutions
Each section opens with the code diff against the lab’s
agent.py, then a common mistake callout, then the broader
lesson. The diffs are meant to drop into agent.py with minimal
edits — copy-paste, re-run python agent.py "…", and you should see the
new behaviour on the very next kickoff.
Solution to Task 1 — insert a fact-checker between worker and reviewer
Define a fourth agent, slot a new task between draft_task and
review_task, and update the reviewer’s context= so it can read the
fact-checker’s findings.
fact_checker = Agent(
role="Fact Checker",
goal="Verify each claim in the draft against the KB entry.",
backstory=(
"You are a literal-minded checker. If a claim isn't in the KB, "
"you flag it. You never speculate."
),
llm=MODEL,
tools=[topic_lookup],
verbose=True,
allow_delegation=False,
)
fact_check_task = Task(
description=(
"Read the draft. For each claim, check it against the KB via "
"topic_lookup({topic}). List any claims not supported by the KB."
),
expected_output="A short list of unsupported claims, or 'all claims supported'.",
agent=fact_checker,
context=[plan_task, draft_task],
)
# review_task gets fact_check_task added to its context:
review_task = Task(
description=(
"Review the draft for unsupported claims or unclear sentences. "
"Take the fact-checker's findings into account."
),
expected_output="A short review: list 0-3 issues, or write 'no issues found'.",
agent=reviewer,
context=[plan_task, draft_task, fact_check_task],
)
# In run_brief_crew:
crew = Crew(
agents=[planner, worker, fact_checker, reviewer],
tasks=[plan_task, draft_task, fact_check_task, review_task],
process=Process.sequential,
verbose=True,
)
Common mistake — forgetting to extend the reviewer’s context
The natural reach is to add fact_check_task to the crew’s tasks=[]
list and stop there. Don’t. CrewAI’s Process.sequential only passes
explicitly-listed prior outputs to a task via context=[…]. If you
leave review_task.context=[plan_task, draft_task] unchanged, the reviewer never
sees the fact-checker’s findings — the new agent silently provides zero value to
downstream tasks. Always update the downstream context= when you insert an agent.
Is the 4-agent crew better or just slower? It depends. The fact-checker is largely a redundant safety net — most of what it catches, the reviewer would have caught anyway. The wins: explicit separation (you can now bill fact-checking cost separately, swap the fact-checker for an automated KB diff later, or route just it through a different model). The losses: ~33% more cost and latency per run. For a high-stakes brief (legal, medical, security advisories) the safety net is worth it; for an internal weekly summary it’s overkill.
The deeper lesson: adding an agent always adds cost — the value has to come from specialisation the existing agents can’t provide, not from “more eyes on the work”. If the new agent’s persona overlaps heavily with an existing one, tighten the existing prompt instead.
Solution to Task 2 — per-agent model routing for a real cost win
Three one-line edits — one per agent’s llm= field. Everything else stays
the same.
planner = Agent(
role="Brief Planner",
goal="Produce a 3-section outline for the brief — one line per section.",
backstory="...",
llm="anthropic/claude-3-5-sonnet-latest", # unchanged — judgement-heavy
tools=[topic_lookup],
verbose=True,
allow_delegation=False,
)
worker = Agent(
role="Brief Writer",
goal="Turn the planner's outline into a structured BriefDraft.",
backstory="...",
llm="anthropic/claude-3-5-haiku-latest", # was sonnet — transformation only
verbose=True,
allow_delegation=False,
)
reviewer = Agent(
role="Skeptical Reviewer",
goal="Flag any unsupported claims or unclear sentences in the draft.",
backstory="...",
llm="anthropic/claude-3-5-sonnet-latest", # unchanged — judgement-heavy
verbose=True,
allow_delegation=False,
)
Run the crew on three topics and watch the cost in out["usage"]. Expect a
~30–40% drop vs. the all-Sonnet baseline. The worker’s draft step
is by far the most token-heavy part of the run (drafts have more output tokens than outlines
or reviews), and Haiku is roughly 5× cheaper per output token than Sonnet. The brief
quality should be essentially unchanged because the worker’s job is a constrained
transformation (outline + KB facts → structured draft), not open-ended reasoning.
Common mistake — routing the reviewer to Haiku to save more money
Don’t. Review requires judgement (is this claim supported? is this sentence clear?) and Haiku is materially more likely to wave through a flawed draft. You’ll save another ~10% on cost and pay it back in undetected errors. The general rule: route the agent doing the most token-heavy transformation to the cheap model; keep judgement-heavy roles on the capable model.
The deeper lesson: per-agent model routing is the underrated CrewAI win. Most
frameworks (LangGraph, OpenAI SDK, hand-rolled loops) make it awkward to pin different models
to different steps — you end up threading model= through factory functions
or maintaining two parallel agent definitions. CrewAI’s per-Agent
llm= field makes it a one-line edit. Use this. It’s a free cost win on every
crew with a heavy transformation step.
Solution to Task 3 — structured ReviewReport output
Define two Pydantic models, attach output_pydantic=ReviewReport to
review_task, and surface the parsed object in run_brief_crew’s
return dict.
from pydantic import BaseModel
class ReviewIssue(BaseModel):
severity: str # "low" | "medium" | "high"
description: str
class ReviewReport(BaseModel):
issues: list[ReviewIssue]
overall: str # "ship" | "revise" | "reject"
review_task = Task(
description=(
"Review the draft. For each issue, give a severity (low/medium/high) "
"and a one-line description. Finish with an overall verdict: "
"'ship', 'revise', or 'reject'."
),
expected_output=(
"A ReviewReport object with `issues` (a list of ReviewIssue with "
"`severity` and `description`) and `overall` (one of 'ship', "
"'revise', 'reject')."
),
agent=reviewer,
context=[plan_task, draft_task],
output_pydantic=ReviewReport,
)
def run_brief_crew(topic: str) -> dict[str, Any]:
crew = Crew(
agents=[planner, worker, reviewer],
tasks=[plan_task, draft_task, review_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff(inputs={"topic": topic})
draft = draft_task.output.pydantic # BriefDraft instance
review = review_task.output.pydantic # ReviewReport instance (or None)
return {
"outline": str(plan_task.output),
"draft": draft.model_dump() if draft else None,
"review": str(review_task.output),
"review_struct": review.model_dump() if review else None,
"final": str(result),
"usage": getattr(crew, "usage_metrics", None),
}
Common mistake — assuming .pydantic is always populated
Even with output_pydantic=ReviewReport, CrewAI 0.86 occasionally returns
review_task.output.pydantic is None if the LLM’s output couldn’t be parsed
into the schema. The fix is twofold: (1) tighten the
expected_output= description — be explicit about the field names and the
valid enum values for overall, as in the diff above — and
(2) defensively guard with review.model_dump() if review else None
so a single bad parse doesn’t crash the whole run. Downstream code that reads
review_struct should also handle the None case.
The deeper lesson: typed task outputs are how multi-agent systems become
programmatically actionable. When the reviewer’s verdict is the string
"ship", downstream code can branch on it directly:
out = run_brief_crew("linux kernel 6.14 networking")
if out["review_struct"] and out["review_struct"]["overall"] == "ship":
publish_brief(out["draft"])
elif out["review_struct"] and out["review_struct"]["overall"] == "revise":
queue_for_revision(out["draft"], issues=out["review_struct"]["issues"])
else:
alert_humans(out)
Without the typed output you’d need another LLM call to extract the verdict from free-form review prose — which is exactly what the typed output saved you. Whenever a task’s output is going to drive a programmatic decision (route, branch, alert), pay the small Pydantic-schema cost upfront and skip the parsing layer downstream.
What you’ve built
Across three small extensions you’ve added specialisation (fact-checker), cost-aware routing (per-agent model selection), and programmatic actionability (typed reviewer output) to the canonical CrewAI multi-agent shape. That’s the production template for crew-based agents in three pieces:
- Specialisation — new agents earn their cost by doing something the existing agents structurally can’t, not by adding “more eyes”.
- Cost-aware routing — pin Haiku to the heavy transformation step, keep Sonnet on the judgement steps; ~30–40% cost win for free.
- Typed outputs at the crew boundary — Pydantic schemas on the last task so downstream code can branch on the result without re-parsing prose.
Closing tip: You’ve now built the canonical CrewAI multi-agent shape. Module 6 puts this crew side-by-side with the Claude SDK research assistant (M2), the LangGraph pipeline (M3), and the OpenAI handoff (M4) to answer “which framework, when?” with real cost, latency, and lines-of-code numbers.