Skip to content

anthropic: trace tool_runner / message threads and fix direct iteration of create(stream=True) #1002

Closed

Description

Sub-issue of #1001 (follow-ups from the 2026-10-26 dependency update, #998).

Do part 2 first: once tool_runner / threads go through our wrapper, the runner's streaming mode iterates create(stream=True) directly.

1. tool_runner and message threads aren't traced at all

  • client.beta.messages.tool_runner(...) and, on 2.2+, client.beta.messages.thread(...) produce zero spans on a wrapped client, in all four modes (sync/async, streaming or not). Reproduced live on anthropic 1.17.0 and 2.3.0.
  • Cause: the Messages wrapper forwards unknown attributes to the raw object (Wrapper.__getattr__ in integrations/anthropic/_utils.py). The runner gets a method bound to the raw client, so every model call in the loop skips our create / stream.
  • Why it matters: the tool runner is Anthropic's recommended agent loop, so users of it get no LLM spans, token metrics or tool spans.
  • Fix: wrap tool_runner and thread on the beta Messages wrapper.
    • Option A: an outer span per run, with child LLM spans by routing the runner's client back through our wrappers.
    • Option B: patch the runner's per-iteration request. In 2.x the loop runs over a thread that calls self._client.beta.messages.create(...), which makes this workable.
    • Carry thread metadata (type, previous_message_id). On follow-up turns the SDK sends only the new messages and drops system / tools, so span input looks incomplete unless it's linked to the earlier turn.
  • Depends on item 2: once these calls go through our wrapper, the runner's streaming mode iterates create(stream=True) directly.
  • Tests: cassette-backed tool_runner tests (sync/async × stream/non-stream) with a real @beta_tool, recorded at latest.

2. Iterating messages.create(stream=True) directly raises TypeError on wrapped clients

  • for event in client.messages.create(..., stream=True) fails with TypeError: 'TracedMessageStreamManager' object is not iterable. Only the with ... as stream: form works. Reproduced on 1.17.0 and 2.3.0. The code works unwrapped and breaks once wrapped.
  • Cause: sync create(stream=True) wraps the result in TracedMessageStreamManager (integrations/anthropic/tracing.py), which only implements __enter__ / __exit__.
  • Fix: return a TracedMessageStream (or give the manager __iter__ / close() / .response) that ends the span when iteration finishes or the stream closes.
    • The async path returns a bare async generator with no .response / .close(). Make it consistent at the same time.
    • Hardening: since anthropic 1.16 the SDK passes along SSE events it doesn't recognize. TracedMessageStream.__process_message calls accumulate_event on every event, which raises if a non-message_start event arrives first. Skip unknown events the way the SDK's own helper does.
  • Tests: red→green cassette test iterating create(stream=True) without with, sync and async.

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions