« 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+1 manager-call shape the lab surfaces as manager_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 sequential and hierarchical that 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_output is 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.
  • context is 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 explicit context=[...], 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=True attaches an AgentPlanner that 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-stamped id) or a Pydantic BaseModel (structured, declared Flow[MyState]). The lab uses a @dataclass in place of Pydantic to keep the dependency out while preserving the semantics (typed fields that mutate and persist).
  • @persist uses SQLiteFlowPersistence, keyed by the state id (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.state and @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 policy instead of a litellm-backed Agent running a real tool-use loop with max_iter/max_rpm, memory, and planning.
  • The manager split into allocate/synthesize pure 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 SQLiteFlowPersistence keyed by a UUID; a @dataclass instead of a Pydantic BaseModel for structured state.
  • kickoff(inputs) only — no kickoff_for_each, _async, or replay; 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.