Skip to content

uipath-claude-sdk: a suspending tool has no trace span on the pass that succeeds #371

Description

@radu-mocanu

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions