Problem
A tool that suspends is invisible in traces on the pass where it actually succeeds. Across a suspension you get one span that is always an error, and no span at all for the run that produced the result.
Observed on a deployed agent whose create_salesforce_record tool is wrapped in an approval gate:
Suspend pass. A span named mcp__salesforce__create_salesforce_record appears with the correct input, and its only recorded outcome is Errors: Abandoned.
Resume pass. No tool span exists. The body demonstrably ran, because its own work is right there in the trace as loose siblings at turn level: folder_get_personal_workspace_async, connections_retrieve, connections_retrieve_token. Nothing groups them, and the tool's return value is recorded nowhere.
Net effect: for every suspending tool, the trace shows a failure that did not happen and hides the success that did. Diagnosing a gated tool means falling back to job logs, and those only capture httpx traffic, so a tool using a requests-based client writes nothing at all.
Why the suspend-pass span is an error
The tool span comes from the CLI hooks that openinference-instrumentation-claude-agent-sdk (0.1.9) registers: PreToolUse opens it, PostToolUse closes it, PostToolUseFailure closes it with an error.
This package executes the tool body inside its own PreToolUse hook and then defers the call, which is the documented design (lines 39-50). A deferred call is never executed by the CLI, so PostToolUse never fires and the span is left open. Abandoned is the tracer closing it out.
That part is arguably correct as a description of the CLI's view. The real gap is the resume pass.
Why the resume pass has nothing
The only @traced in the module is SuspendChannel.claim (line 275), which by definition runs on the suspend pass only, since it is called from the except _InterruptRaised branch.
run_tool_body (line 467) is not traced. It is the function that actually executes the developer's body, on both passes, at line 493. So there is no span representing the execution that returns a result.
The body's children still attach to the turn, because _under_turn(channel) attaches channel.turn_parent, which is why they appear at turn level rather than orphaned. They just have no parent of their own.
I did not determine whether the CLI re-issues a replayed call through PreToolUse on the resume pass, or whether the instrumentor simply cannot match a tool_use_id its in-memory tracker never opened (the tracker does not survive the process boundary either way). That distinction does not change the ask below, since neither path can be fixed from inside this package.
Suggested change
Trace run_tool_body. It is the one place that knows the tool name, the arguments, the tool_use_id, and the outcome, and it runs on every pass:
@traced(name="claude_tool_body", run_type="uipath")
async def run_tool_body(channel, binding, args, tool_name, tool_use_id): ...
That yields a span on both passes, gives the body's existing child spans a correct parent, and records the return value on the pass that produced it. Suggested attributes: tool_name, tool_use_id, and whether the pass suspended or resolved, which is already known from channel.is_resolved(tool_use_id).
Two notes on shape. An input processor is probably wanted, matching the existing _identifying_inputs_only used on claim, since tool arguments can carry record data. And distinguishing "suspended" from "completed" on the span would let a reader tell the two passes apart, which is the thing that is impossible today.
This does not require the abandoned CLI-level span to change. A correct UiPath-side span next to it is enough to make a gated tool diagnosable.
Environment
uipath-claude-sdk as on main, byte-identical to the build I traced against
openinference-instrumentation-claude-agent-sdk 0.1.9
- Python 3.14
- Line numbers above are against
main
Problem
A tool that suspends is invisible in traces on the pass where it actually succeeds. Across a suspension you get one span that is always an error, and no span at all for the run that produced the result.
Observed on a deployed agent whose
create_salesforce_recordtool is wrapped in an approval gate:Suspend pass. A span named
mcp__salesforce__create_salesforce_recordappears with the correct input, and its only recorded outcome isErrors: Abandoned.Resume pass. No tool span exists. The body demonstrably ran, because its own work is right there in the trace as loose siblings at turn level:
folder_get_personal_workspace_async,connections_retrieve,connections_retrieve_token. Nothing groups them, and the tool's return value is recorded nowhere.Net effect: for every suspending tool, the trace shows a failure that did not happen and hides the success that did. Diagnosing a gated tool means falling back to job logs, and those only capture
httpxtraffic, so a tool using arequests-based client writes nothing at all.Why the suspend-pass span is an error
The tool span comes from the CLI hooks that
openinference-instrumentation-claude-agent-sdk(0.1.9) registers:PreToolUseopens it,PostToolUsecloses it,PostToolUseFailurecloses it with an error.This package executes the tool body inside its own
PreToolUsehook and then defers the call, which is the documented design (lines 39-50). A deferred call is never executed by the CLI, soPostToolUsenever fires and the span is left open.Abandonedis the tracer closing it out.That part is arguably correct as a description of the CLI's view. The real gap is the resume pass.
Why the resume pass has nothing
The only
@tracedin the module isSuspendChannel.claim(line 275), which by definition runs on the suspend pass only, since it is called from theexcept _InterruptRaisedbranch.run_tool_body(line 467) is not traced. It is the function that actually executes the developer's body, on both passes, at line 493. So there is no span representing the execution that returns a result.The body's children still attach to the turn, because
_under_turn(channel)attacheschannel.turn_parent, which is why they appear at turn level rather than orphaned. They just have no parent of their own.I did not determine whether the CLI re-issues a replayed call through
PreToolUseon the resume pass, or whether the instrumentor simply cannot match atool_use_idits in-memory tracker never opened (the tracker does not survive the process boundary either way). That distinction does not change the ask below, since neither path can be fixed from inside this package.Suggested change
Trace
run_tool_body. It is the one place that knows the tool name, the arguments, thetool_use_id, and the outcome, and it runs on every pass:That yields a span on both passes, gives the body's existing child spans a correct parent, and records the return value on the pass that produced it. Suggested attributes:
tool_name,tool_use_id, and whether the pass suspended or resolved, which is already known fromchannel.is_resolved(tool_use_id).Two notes on shape. An input processor is probably wanted, matching the existing
_identifying_inputs_onlyused onclaim, since tool arguments can carry record data. And distinguishing "suspended" from "completed" on the span would let a reader tell the two passes apart, which is the thing that is impossible today.This does not require the abandoned CLI-level span to change. A correct UiPath-side span next to it is enough to make a gated tool diagnosable.
Environment
uipath-claude-sdkas onmain, byte-identical to the build I traced againstopeninference-instrumentation-claude-agent-sdk0.1.9main