« Phase 19 README · Warmup · Hitchhiker's · Deep Dive · Principal Deep Dive · Staff Notes
Phase 19 — Core Contributor Notes: How CrewAI Is Actually Built
This is the maintainer's-eye view of the real crewai package, not our miniature. Where the lab
injects a policy and splits the manager into two pure functions, real CrewAI compiles personas into
system prompts, runs a full tool-use loop per task through a provider-agnostic LLM layer, and ships a
memory subsystem, a planner, and a SQLite-backed Flow persistence store. The interesting engineering
is in the seams. Where I describe a pattern rather than a verified internal, I say so — CrewAI moves
fast, so treat versioned specifics as "check the source."
The agent is a persona compiled to a prompt, wrapping a real executor
The Agent's role / goal / backstory are not decoration — CrewAI assembles them into the
system prompt that frames every task the agent takes on, then appends the task's description and
expected_output. That is the real mechanism behind the lab's render_prompt. Around that prompt is
an executor that runs the tool-use loop: propose a tool call, execute it, feed the observation
back, iterate until the agent produces the task output. A committer's non-obvious takeaway: a
"3-task crew" makes far more than 3 LLM calls, because each task is its own ReAct loop, and CrewAI
bounds it with per-agent knobs like max_iter and max_rpm. allow_delegation=True additionally
grants the agent CrewAI's built-in Delegate work to coworker and Ask question to coworker tools —
delegation is implemented as a tool, which is exactly how the hierarchical manager hands work out.
The LLM layer: from LangChain to an independent, litellm-backed core
The most significant design evolution is under the hood, not in the API. Early CrewAI leaned on
LangChain for the agent executor and LLM plumbing; later versions moved to an independent execution
core routed through litellm, so a model is a string/LLM object and the same crew runs against
100-plus providers without changing the crew code. Why it changed: owning the loop and using a thin
provider-abstraction shed a heavy dependency, made the execution path debuggable, and let CrewAI
control retries, timeouts, and token accounting itself. The lesson for a maintainer: the framework's
value proposition ("cast a team, call kickoff") is stable, but the substrate under it was rebuilt to
control the exact parts that matter for production — the loop and the LLM boundary.
The hierarchical manager is a real Agent, not two functions
The lab splits the manager into allocate and synthesize pure functions so each is independently
testable. Real CrewAI builds a manager Agent — either auto-constructed from manager_llm or the
one you pass as manager_agent — with allow_delegation and a manager-specific system prompt, and
it coordinates by using the delegation tools. The consequences a committer cares about:
- Delegation is addressed by role string. The manager names a coworker by its
role, so roles should be unique and the manager can name one that does not exist. CrewAI matches the role and a mismatch is surfaced/retried rather than silently dropped — the same "unknown worker is an error" invariant the lab enforces as a hard raise. - The manager cost is structural. Every delegation is a manager reasoning step and the final
reconciliation is another, which is the
N+1manager-call shape the lab surfaces asmanager_calls. In the real system it is even larger because the manager's own reasoning may loop. - A third process was reserved but not shipped. CrewAI's process space has, at times, named a
"consensual" process alongside
sequentialandhierarchicalthat was not implemented — a reminder that the two real engines are the two you should reason about, and to check the source before relying on anything else.
Tasks carry more contract than the lab shows
The lab's Task keeps the load-bearing fields (description, expected_output, agent, context).
Real CrewAI tasks also carry tools, output_json / output_pydantic for typed/validated output
(Phase 02's structured-output discipline), callback, async_execution, human_input, and output
guardrails that validate or transform a task's result before it flows on. Two sharp edges every
CrewAI user eventually hits:
expected_outputis load-bearing, not cosmetic. It is the contract that sharpens the model's target; undersell it and the agent free-associates. Interviewers probe this precisely because it looks optional and is not.contextis the top silent-bug source. The feed-forward default means a two-task pipeline "just works," but a fan-in (a summarizer reading two specific reports) requires an explicitcontext=[...], and a forward reference (depending on a later task) is an error. The lab's three-way switch is faithful to this exactly.
kickoff has a family, and Crews have memory and planning
The lab implements kickoff(inputs). Real CrewAI ships a family: kickoff_for_each (run the crew
once per input row), the _async variants, and replay (re-run from a specific task id using stored
outputs) — all of which exist because production crews run over batches and need to resume. Two
subsystems the lab omits entirely but a committer should know:
- Memory. CrewAI has short-term memory (RAG over recent interactions via embeddings), long-term memory (persisted to SQLite across runs), and entity memory. It requires an embedder config and adds cost and latency — a real tradeoff, not free context.
- Planning.
planning=Trueattaches anAgentPlannerthat pre-plans task steps before execution — an extra LLM pass that trades tokens for a more coherent run.
Both are "the framework does more than the lab, and each addition has a token/latency cost you must budget."
Flows: the orchestration layer added after Crews
Flows arrived after Crews, to fill the gap Crews structurally cannot: branching, waiting on external events or humans, composing multiple crews, and durability. That is why the CrewAI docs have a dedicated "Crews vs Flows / when to use each" guide — users kept conflating a task sequence with an event graph. The real mechanics the lab mirrors faithfully:
- The event graph is built by introspection of the decorated methods (
@start,@listen,@router,or_/and_), referenced by function identity in the class body — the same wiring the lab does by__name__. - State is
dict(unstructured, auto-stampedid) or a PydanticBaseModel(structured, declaredFlow[MyState]). The lab uses a@dataclassin place of Pydantic to keep the dependency out while preserving the semantics (typed fields that mutate and persist). @persistusesSQLiteFlowPersistence, keyed by the stateid(a UUID). The lab uses an in-memory store keyed by an injected counter id — identical save/load-by-id interface, deterministic for tests. This is what makes human-in-the-loop practical: persist on pause, resume hours later from exactly where it stopped.- A router emits a label, not data. Real CrewAI branches on the router's return value the same
way; data travels through
self.stateand@listen(method)payloads. Expecting a router to hand data to a branch is the classic Flow misconception.
CrewAI also exposes plot() to visualize the flow graph — the productized form of the lab's
completed trail.
Config-as-data and the YAML pattern
A production idiom the lab skips: CrewAI supports declaring agents and tasks in agents.yaml /
tasks.yaml and binding them with @CrewBase and @agent / @task / @crew decorators. This is
the "config, not code" pattern — the persona and task contracts become reviewable data, which is the
same instinct as checking specs into a repo.
What our miniature deliberately simplifies
- An injected
policyinstead of a litellm-backedAgentrunning a real tool-use loop withmax_iter/max_rpm, memory, and planning. - The manager split into
allocate/synthesizepure functions instead of a real manager Agent wielding the delegation tools with its own prompt and reasoning loop. - An in-memory flow store keyed by a counter id instead of
SQLiteFlowPersistencekeyed by a UUID; a@dataclassinstead of a PydanticBaseModelfor structured state. kickoff(inputs)only — nokickoff_for_each,_async, orreplay; no memory subsystem, no planner, no YAML config, no output guardrails.- Arity-based payload passing as a faithful stand-in for CrewAI's signature handling.
Know that delegation is a tool, that the manager is a real Agent with a real cost, that Flows were added to fill the orchestration gap Crews cannot, and that persistence is SQLite-by-id — and the real package's docs read as confirmation rather than surprise.