My Claude Agent SDK agent fails without raising an error. Why?
In Rius, a Claude Agent SDK run that fails without raising an error shows as a completed trace, because trace status comes from the spans and the runtime’s inner steps record no spans. Open the run’s generation span to see what the turn produced, and turn on the Swallowed Error alert to catch runs where a step errored but the run still reported success. Filtering the trace list to failed traces shows the runs where a child span errored and your code recovered.
How it works
Rius derives trace status from the spans, not from anything your agent reports. A trace reads completed when no span carries an error status, and failed when at least one does.
The Claude Agent SDK integration is manual, because the SDK drives the Claude Code runtime in a separate process. A task produces two spans:
- The root
AGENTspan, from yourobservewrapper. - One generation for the whole runtime turn, with model, tokens and cost, filled from the run’s
ResultMessage.
Tool calls, sub-turns and file operations inside the Claude Code process produce no spans. A failure there leaves no error span, so the trace reads completed and your function returns normally. The SDK does not raise for a failed run either: it reports it on the ResultMessage, whose is_error is true and whose subtype names the reason, such as error_max_turns. The example code below does not read either field.
There is a second path. If your own code catches an exception and returns a fallback, the root span finishes OK. When a child span errored, the trace is failed but badged recovered, because the root completed clean. The errors are still counted. The Swallowed Error alert targets this pattern: the root finishes OK while at least one child step that is not a tool call ended with an error. In this integration the generation is the only child of the root. Errored tool calls never count toward the alert, because a failed tool call goes back to the model as its result.
Set it up
- Check that the root span and the generation are both in place. This is the integration from the Claude Agent SDK page, and it is what gives you a span to inspect.
pip install glassflow-rius claude-agent-sdkimport rius
from claude_agent_sdk import (
AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query,
)
from rius import SpanKind
rius.init(api_key="ri_...", service_name="my-agent")
MODEL = "claude-haiku-4-5"
@rius.observe(name="handle-task", kind=SpanKind.AGENT)
async def handle_task(task: str) -> str:
options = ClaudeAgentOptions(model=MODEL, max_turns=1)
parts = []
with rius.start_as_current_generation(
"claude_agent_turn", model=MODEL, provider="anthropic", input=task
) as gen:
async for message in query(prompt=task, options=options):
if isinstance(message, AssistantMessage):
parts.extend(
b.text for b in message.content if isinstance(b, TextBlock)
)
elif isinstance(message, ResultMessage):
usage = message.usage or {}
gen.set_usage(
input_tokens=usage.get("input_tokens"),
output_tokens=usage.get("output_tokens"),
cache_read_input_tokens=usage.get("cache_read_input_tokens"),
# Anthropic's payload still calls the write count "cache creation"
cache_write_input_tokens=usage.get("cache_creation_input_tokens"),
)
models = list((message.model_usage or {}).keys())
if models:
gen.set_response_model(models[0])
answer = "".join(parts)
gen.set_output(answer)
return answer-
Record the runtime’s own verdict. In the
ResultMessagebranch, addif message.is_error: gen.record_exception(RuntimeError(message.subtype)). The generation then carries an ERROR status, so the trace readsfailed, and a run your code still returns from counts toward Swallowed Error. -
Look for recovered runs first. On Traces, filter the status to
failed. Rows badged recovered are runs where a child span errored and the root still finished clean. Open one: failed spans are highlighted along the path from the root to the error, and the detail panel leads with the exception and traceback. See Exploring traces for the filters. -
If the trace is
completed, open the generation. On the Input / Output tab, read the prompt and the collected answer. An empty or partial answer is the failure your code did not raise. Check tokens and duration too. A generation with no cost means an alias such ashaikuwas used as the model id instead of a full id such asclaude-haiku-4-5. -
Catch it next time. On the pre-defined alerts page, make sure Swallowed Error is enabled and has a channel attached. It fires once per run, and only for finished traces. If the same error recurs across runs, Consistent Error Pattern fires by default when at least 3 errored spans of the same error span at least 2 distinct traces within 5 minutes.
What you get
- A clear reading of the symptom. A
completedtrace with no error span means nothing in your process recorded a failure. A recovered badge means an error was caught and the run carried on. - One generation per task with the model, tokens, cost, prompt and answer, so you can judge the turn’s result directly.
- An alert for the next occurrence. Swallowed Error flags a run that looks successful while a non-tool step failed. Consistent Error Pattern flags the same error repeating across runs.
- A starting point for the wider setup in Monitor AI agents in production.
When something else fits
- If the failure happens inside the Claude Code runtime, such as a tool call or file operation, this integration records no span for it. The generation holds only the turn’s totals, so judge those runs by the answer and the totals.
- If you want per-step spans for your own code, wrap those steps in
observeyourself. Frameworks whose model calls are auto-instrumented are listed on Integrations.