jimbobbennett/crewai-openinference-tool-span-repro

Reproduction: openinference-instrumentation-crewai does not emit TOOL spans for @tool-decorated tools

★ 1Forks 0PythonGitHub ↗Compare

README

CrewAI tool spans are missing with openinference-instrumentation-crewai

openinference-instrumentation-crewai does not emit a TOOL span when an agent calls a tool defined with CrewAI's @tool decorator. The trace shows the crew (CHAIN), the agent (AGENT) and the LLM calls (LLM), but the tool invocation is invisible: there is no span with openinference.span.kind == "TOOL" and therefore no tool input/output in the trace, even though the tool clearly ran.

This reproduces with the current release (openinference-instrumentation-crewai 1.1.10) on CrewAI 1.x using native tool calling.

Versions

  • crewai 1.14.2
  • openinference-instrumentation-crewai 1.1.10
  • openinference-instrumentation-anthropic 1.0.0
  • arize-otel 0.11.0
  • opentelemetry-api 1.34.1 / opentelemetry-sdk 1.34.1
  • anthropic 0.116.0
  • Python 3.12

Full pinned set in requirements.txt.

Running it

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

cp .env.example .env   # add ANTHROPIC_API_KEY (ARIZE_* optional)

python main.py

main.py is a minimal crew: one Agent, one @tool-decorated current_time tool, one Task, run with Crew.kickoff(). It sets up the stock OpenInference CrewAI + Anthropic instrumentors — no patch, no manually created tool span.

To make the bug visible without an Arize account, the script also attaches an in-process SimpleSpanProcessor with a small exporter that records each span's name and openinference.span.kind, and prints them at the end. If ARIZE_SPACE_ID / ARIZE_API_KEY are set it exports to Arize as well (that is how the screenshot below was produced); if they are absent it still runs and prints the local span kinds.

.env.example sets CREWAI_DISABLE_TELEMETRY=true. CrewAI's own telemetry emits OTel spans with no openinference.span.kind, so disabling it keeps the trace to just the OpenInference spans and makes the missing TOOL span unambiguous. Do not use OTEL_SDK_DISABLED for this — it also stops the Arize export.

Expected vs actual

Expected: the trace contains a TOOL span for the current_time call, with its input and output.

Actual: no TOOL span is produced. The agent still calls the tool (the final answer contains the real current time), but the call is not traced.

Observed span-kind output from a run (ANTHROPIC_API_KEY set, Arize export off):

=== Span kinds seen this run ===
  LLM          messages.create
  LLM          messages.create
  AGENT        Friendly Greeter._execute_core
  CHAIN        Crew_dfb0d01f-1c52-4b97-a615-7d752205b0b3.kickoff

No span with openinference.span.kind == "TOOL" was emitted, even though the agent called the current_time tool.

Two LLM spans (the tool-calling round trip), one AGENT, one CHAIN, and no TOOL — despite the greeting correctly reporting the time returned by the tool.

Screenshot

The same run exported to Arize. The trace has the crew, agent and LLM spans but no tool span:

Arize trace showing CHAIN, AGENT and LLM spans but no TOOL span for the current_time tool

Root cause

The instrumentor wraps crewai.tools.base_tool.BaseTool.run:

# openinference/instrumentation/crewai/__init__.py (1.1.10), ~line 234
wrap_function_wrapper(
    "crewai.tools.base_tool",
    "BaseTool.run",
    base_tool_run_wrapper,
)

But CrewAI's @tool decorator produces a crewai.tools.base_tool.Tool instance, and Tool overrides run() without calling super().run():

# crewai/tools/base_tool.py
class Tool(BaseTool, Generic[P, R]):     # ~line 461
    def run(self, *args, **kwargs):      # ~line 472
        ...
        result = self.func(*args, **kwargs)   # calls the wrapped function directly
        ...
        return result

CrewAI 1.x's native tool-calling loop registers each tool's bound run as the callable and invokes it:

# crewai/utilities/agent_utils.py, line 211
available_functions[sanitized_name] = tool.run

For a @tool tool, tool is a Tool, so available_functions[...] is the unwrapped Tool.run. The instrumentor only patched BaseTool.run, which Tool.run shadows and never calls, so the wrapper never fires and no TOOL span is created.

Wrapping Tool.run (in addition to BaseTool.run) would cover the @tool path.

Contributors

jimbobbennett

Issues