Skip to Content

How do I trace a research agent built with CrewAI?

Rius traces a CrewAI research agent with the openai integration for the model calls and observe wrappers for the run, its tools and each stage. Install the extra, call rius.init(), then wrap the entrypoint as an AGENT span and your tool bodies as TOOL spans. Add a retrieval span for your knowledge base and scope each job with a session and a user. The CrewAI integration is Python only.

How it works

CrewAI 1.x drives the OpenAI client directly, so the openai integration captures each model call as a generation with model, tokens, cost and time to first token. Cost is a list-price estimate in US dollars that Rius computes, so you never send it. If LiteLLM is installed, CrewAI uses it instead, and you use the litellm integration. The CrewAI integration page covers both cases, and the Integrations page lists every framework Rius covers.

The structure of a research run happens inside CrewAI, where auto-instrumentation cannot see it. Crews and tasks produce no spans of their own, and generation spans share the generic name ChatCompletion. You add the shape of your research agent with observe and span helpers:

  • A run root. Wrap your entrypoint with observe and SpanKind.AGENT. It becomes the root span, and every model call and tool below it nests under it.
  • A step per stage. Wrap each stage, such as gathering sources and writing the report, in its own observe function. A bare observe makes a span named after the function, so you can see which stage a model call belongs to.
  • Retrieval. Open a span with SpanKind.RETRIEVER, a data_source_id and top_k. Record the documents with set_retrieved_documents(), which stores ids and scores, not document text.
  • Tools. Put observe with SpanKind.TOOL on the tool body, with @tool outermost. Only TOOL spans show under the Tool kind in the Spans tab and count toward the Tool Loop alert.

Set it up

  1. Install the integration and CrewAI:
pip install "glassflow-rius[openai]" crewai
  1. Initialize Rius, wrap the tool, the retrieval step, each stage and the run root. The names web_search, research-notes, gather-sources, write-report, run-research and the helpers run_search and search_notes are examples of your own code, so swap in yours:
import rius from crewai import Agent, Crew, Task from crewai.tools import tool from rius import SpanKind rius.init(api_key="ri_...", service_name="research-agent") @tool("web_search") @rius.observe(kind=SpanKind.TOOL) def web_search(query: str) -> str: """Search the web for a query.""" return run_search(query) researcher = Agent( role="Researcher", goal="Find accurate sources for the question.", backstory="You research topics and cite what you find.", tools=[web_search], llm="gpt-5-mini", ) def retrieve(question: str): with rius.start_as_current_span( kind=SpanKind.RETRIEVER, data_source_id="research-notes", top_k=5 ) as obs: obs.set_input(question) docs = search_notes(question) obs.set_retrieved_documents([{"id": d.id, "score": d.score} for d in docs]) return docs @rius.observe(name="gather-sources") def gather_sources(question: str, context: str) -> str: task = Task( description=f"Find sources for: {question}\n\nNotes so far:\n{context}", expected_output="A list of sources with notes.", agent=researcher, ) return str(Crew(agents=[researcher], tasks=[task]).kickoff()) @rius.observe(name="write-report") def write_report(notes: str) -> str: task = Task( description=f"Write a short report from these notes: {notes}", expected_output="A concise report.", agent=researcher, ) return str(Crew(agents=[researcher], tasks=[task]).kickoff()) @rius.observe(name="run-research", kind=SpanKind.AGENT) def run_research(question: str) -> str: docs = retrieve(question) notes = gather_sources(question, "\n".join(d.text for d in docs)) return write_report(notes) with rius.session(job_id), rius.user(requester_id): run_research("How do vector indexes affect recall?")
  1. Scope each job with rius.session(), passing your own job or conversation id, so every span of the run carries session.id. Scope the requester with rius.user(), and prefer an opaque id over an email. Without a session, each trace is its own session.

  2. Set CREWAI_TRACING_ENABLED=false where no terminal is attached, such as CI or containers, because CrewAI asks about its own tracing on first run.

  3. Run one question that calls a tool, then open the trace in the console. You should see the root AGENT span, your stage spans, the retrieval span, the LLM generations and your named TOOL spans. Streamed calls need usage enabled on the underlying OpenAI client to carry token counts.

What you get

  • A single trace per research run, rooted at run-research, with a span for each stage you wrapped.
  • A generation for every model call, with model, tokens, cost and time to first token.
  • A retrieval span that shows which knowledge base was searched, how many documents were requested and the ids and scores returned.
  • Named TOOL spans that tie each tool call to the run.
  • Sessions that group a job’s traces, and a Users view that follows each requester’s traces, failures and cost.

When something else fits

  • If LiteLLM is installed, CrewAI calls models through it, so enable the litellm integration for the model calls.
  • There is no CrewAI integration for TypeScript, so trace those model calls with manual generations as described in Tracing your code.
  • If you need to know which task a model call belongs to, wrap each task in its own observe function, as the stage wrappers above do.

Still stuck? Get help

Last updated on