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.
- 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.
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.pymain.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: 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.
The same run exported to Arize. The trace has the crew, agent and LLM spans but no tool span:
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 resultCrewAI 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.runFor 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.
