AETIUS

CrewAI adapter

Mapping

CrewAI's native model is agents plus tasks under a Process: sequential (a fixed order, each task's output can feed the next) or hierarchical (a manager agent delegates to workers). agentstage.crewai.adapt(system) picks between them from OrchestrationGraph.pattern:

  • pipeline maps to Process.sequential. Tasks are built in the order the observed edges walk from entry_role, each carrying the previous task as context, so CrewAI passes its output forward the same way the real system's handoff chain did.
  • supervisor_delegates maps to Process.hierarchical. The hub role becomes manager_agent. CrewAI's own validator, confirmed against its real source before writing this adapter, raises if a manager agent also appears in the agents list, so the hub is built once and only passed as manager_agent, never included in agents.
  • single_agent maps to Process.sequential with one agent and one task.
  • unclassified refuses by default. Passing process=Process.sequential or process=Process.hierarchical (with manager_role) explicitly opts in.

The real gap here, compared to the other two frameworks: CrewAI's Agent requires role, goal, and backstory; its Task requires description and expected_output. Reconstruction only ever observes behavior, which tools got called and which role handed off to which, never a role's stated goal or what a specific task's expected output looks like. Langfuse traces do not carry either. backstory is the one field with a legitimate trace-sourced value, the reconstructed system prompt, when system_prompt_source == "observed". When it is "unavailable", the disclosure placeholder text is deliberately not passed through as backstory, since CrewAI folds backstory into the actual prompt the model sees and injecting placeholder text there would read to the model as real instructions. goal, task description, and task expected_output have no trace-sourced value at all: adapt() requires them explicitly per role and raises MissingRoleFieldError naming exactly which roles are missing which field, rather than inventing plausible-sounding text.

End to end example

Python
import os

os.environ.setdefault("OPENAI_API_KEY", "sk-test-not-a-real-key")

from examples.fixtures import build_environment

env = build_environment()
team_system = env.reconstruct("support-team")

Calling adapt() with nothing supplied for goals, task_descriptions, or expected_outputs:

Python
from agentstage.crewai import adapt
from agentstage.errors import MissingRoleFieldError

try:
    adapt(team_system, goals={}, task_descriptions={}, expected_outputs={})
except MissingRoleFieldError as e:
    print(f"{type(e).__name__}: {e}")
    print("field:", e.field)
    print("missing_roles:", e.missing_roles)
Output
MissingRoleFieldError: crewai requires 'goal' per role, which reconstruction has no source for (traces carry observed behavior, not stated intent) — missing for role(s): ['researcher', 'supervisor', 'writer']
field: goal
missing_roles: ['researcher', 'supervisor', 'writer']

goal is checked first, so that is the field named. Supplying all three fields for all three roles:

Python
crew = adapt(
    team_system,
    goals={
        "supervisor": "Route support tickets to the right specialist and combine their answers.",
        "researcher": "Find the exact policy that answers the question.",
        "writer": "Draft a clear, direct reply using the researcher's findings.",
    },
    task_descriptions={
        "supervisor": "Handle the incoming support ticket end to end.",
        "researcher": "Research the policy needed to answer the ticket.",
        "writer": "Write the reply to the customer.",
    },
    expected_outputs={
        "supervisor": "A resolved ticket with a clear reply.",
        "researcher": "The relevant policy excerpt or document reference.",
        "writer": "A drafted reply ready to send.",
    },
)
print(crew.process)
print("manager_agent:", crew.manager_agent.role)
print("agents:", [a.role for a in crew.agents])
print("tasks:", [t.agent.role for t in crew.tasks])
Output
Process.hierarchical
manager_agent: supervisor
agents: ['researcher', 'writer']
tasks: ['researcher', 'writer']

supervisor is the manager, not a member of agents, exactly what CrewAI's Process.hierarchical requires. researcher and writer are its direct reports, each with their own task.

Running against example inputs

Actually calling crew.kickoff() invokes a real LLM through CrewAI's own client. This documentation does not have real model credentials, and would not spend real API calls even if it did. What can be shown for real, without any of that, is the thing this adapter is actually responsible for: the tools attached to each agent are the real, replay-backed reconstructed tools. Calling one directly, the same way CrewAI calls it internally during a run:

Python
researcher_agent = next(a for a in crew.agents if a.role == "researcher")
search_tool = next(t for t in researcher_agent.tools if t.name == "search_docs")
print(search_tool.run(query="refund policy for annual plans"))
print(search_tool.run(query="something completely unrelated to any of this"))
Output
{'results': ['refund-policy.md#annual-plans']}
{'status': 'unavailable', 'tool': 'search_docs', 'threshold': 0.5, 'closest_similarity': 0.29333333333333333, 'closest_candidate_arguments': {'query': 'refund policy for annual plans'}, 'reason': 'closest observed call fell below the similarity threshold'}

The first call replays a real historical response. The second, an unrelated query, gets the same informative unavailable payload the core concepts page showed for replay_tool_call directly, since that is exactly what this tool's entrypoint calls underneath. Whatever real model CrewAI's Process.hierarchical run eventually calls this tool with, it will see the same real replay behavior, not a fabricated answer.

Using a Braintrust-sourced system

Everything above used the Langfuse-shaped fixture. agentstage.crewai.adapt() does not know or care which source built the ReconstructedSystem it is given. No live Braintrust project was available while writing this page either, so this uses examples/braintrust_fixtures.py, the same disclosed-fixture pattern as docs/sources/braintrust.md.

Python
from examples.braintrust_fixtures import build_environment as build_braintrust_environment

bt_env = build_braintrust_environment()
bt_team_system = bt_env.reconstruct("supervisor")
print("roles:", [r.role for r in bt_team_system.roles])

bt_crew = adapt(
    bt_team_system,
    goals={
        "supervisor": "Route support tickets to the right specialist and combine their answers.",
        "researcher": "Find the exact policy that answers the question.",
    },
    task_descriptions={
        "supervisor": "Handle the incoming support ticket end to end.",
        "researcher": "Research the policy needed to answer the ticket.",
    },
    expected_outputs={
        "supervisor": "A resolved ticket with a clear reply.",
        "researcher": "The relevant policy excerpt or document reference.",
    },
)
print(bt_crew.process)
print("manager_agent:", bt_crew.manager_agent.role)
print("agents:", [a.role for a in bt_crew.agents])
print("tasks:", [t.agent.role for t in bt_crew.tasks])

bt_researcher_agent = next(a for a in bt_crew.agents if a.role == "researcher")
bt_search_tool = next(t for t in bt_researcher_agent.tools if t.name == "search_docs")
print(bt_search_tool.run(query="refund policy"))
print(bt_search_tool.run(query="something completely unrelated to any of this"))
Output
roles: ['researcher', 'supervisor']
Process.hierarchical
manager_agent: supervisor
agents: ['researcher']
tasks: ['researcher']
{'results': ['refund-policy.md']}
{'status': 'unavailable', 'tool': 'search_docs', 'threshold': 0.5, 'closest_similarity': 0.2413793103448276, 'closest_candidate_arguments': {'query': 'refund policy'}, 'reason': 'closest observed call fell below the similarity threshold'}

The mapping itself does not differ: Process.hierarchical, manager_agent exclusion, and real replay-backed tool dispatch (a hit and a miss) all behave exactly as they do for the Langfuse-sourced example above. The one real difference here is in the fixture's own content, not the adapter: this Braintrust-sourced support team only ever had a researcher, no writer, so agents and tasks are one role shorter than the Langfuse-sourced example's.