Why does my Claude Agent SDK agent keep calling the same tool?
Rius shows a Claude Agent SDK run as one task span with one generation that carries the turn’s duration, tokens and cost, so a loop shows up as an unusually large turn. The individual tool calls happen inside the Claude Code runtime and are not recorded as spans, so Rius cannot show which tool repeated or why. The Tool Loop alert counts only spans of kind TOOL, so it fires for tool calls you trace yourself, not for the runtime’s own.
How it works
The Claude Agent SDK drives the Claude Code runtime in a separate process, so Rius records two things you create yourself:
- An
observewrapper makes each task the root AGENT span. - A manual generation wraps the
query()loop and is filled from the run’s result: token usage, the model id and the answer. Rius computes cost from that.
A task looks like this in the trace:
handle-task AGENT 5.0s
└─ claude_agent_turn LLM 5.0s claude-haiku-4-5, tokens + costTool calls, sub-turns and file operations inside the runtime produce no spans. The generation records the turn’s totals only. A looping agent therefore appears as a turn with far more tokens, cost and duration than a normal run of the same task, not as a row of identical tool spans.
The Tool Loop alert fires when the same tool is called with identical parameters at least 3 times in a row inside one trace. It counts only TOOL spans, and it ignores calls whose arguments were not captured. Because the runtime’s calls are not spans, that alert has nothing to count from the integration alone. For the wider picture, see Monitor AI agents in production.
Set it up
- Check that the integration is set up as the Claude Agent SDK page shows: an
observeroot withkind=SpanKind.AGENT, a generation withprovider="anthropic", and a full model id such asclaude-haiku-4-5, not an alias. An alias gives a generation with no cost, which hides how expensive a loop was. - Open Traces and find the slow or costly run. In the trace view, read the generation’s duration, tokens and cost. Use Compare runs to set it beside a normal run of the same agent and see where cost or latency moved.
- Open the Spans tab and choose Tool in the All kinds menu. Tool calls made inside the Claude Code runtime do not appear here, because the integration does not create them.
- To make repeated calls visible, trace your own tool functions with the TOOL kind. A bare
observemakes a CHAIN span, which the Tool kind filter and Tool Loop never see.search_docsbelow is a placeholder for your own function:
import rius
from rius import SpanKind
@rius.observe(name="search_docs", kind=SpanKind.TOOL)
def search_docs(query: str) -> str:
...- Keep content capture on for those tool spans. Tool Loop treats calls as identical only when the tool name and normalized arguments match, and it cannot compare arguments that were not captured. The alert’s defaults are 3 identical calls within a 10 minute window, with at most one other tool call between repeats, and one firing per trace and tool.
What you get
- A trace per task with the turn’s duration, tokens and a list-price cost estimate in US dollars, so a runaway turn stands out against your normal runs.
- Side-by-side comparison of a looping run and a healthy one on the trace page.
- For tool functions you wrap with the TOOL kind, spans that the Spans tab can filter and that Tool Loop watches, with a notification when the same call repeats in one trace.
- A clear picture of the boundary: the runtime’s internal steps are not in the trace, so the trace shows that a loop cost you tokens and time, not which internal call repeated.
When something else fits
- If you need to see each tool call the runtime makes, the integration does not provide it, so read the Claude Code runtime’s own output alongside the Rius trace.
- If your agent is built on a framework that Rius auto-instruments, the run structure arrives from that framework’s own spans. See Integrations for the full list.